feat: 将项目从nano重命名为beaver并更新相关配置
- 将所有环境变量前缀从NANO_改为BEAVER_ - 更新README.md文档内容,包括项目介绍、组件说明和快速开始指南 - 修改.gitignore文件,添加auth-portal运行时路径排除规则 - 更新app-instance镜像标签从nano/app-instance改为beaver/app-instance - 增强技能安全检查器,支持工具前缀白名单功能 - 添加技能草稿重新检查安全性API端点 - 扩展证据选择器,收集工具调用名称用于技能学习 - 改进技能合成器,基于实际调用的工具生成工具提示 - 优化路由超时处理机制,增加重试逻辑 - 更新后端架构文档,添加可视化入口和基础概念说明 - 实现在WebSocket消息中传递工具迭代次数信息
This commit is contained in:
@ -34,12 +34,8 @@ PROXY_RELOAD_SCRIPT = Path(
|
||||
).resolve()
|
||||
|
||||
API_TOKEN = os.environ.get("DEPLOY_CONTROL_API_TOKEN", "").strip()
|
||||
INSTANCE_IMAGE = os.environ.get("APP_INSTANCE_IMAGE", "nano/app-instance:latest").strip()
|
||||
INSTANCE_NETWORK_NAME = os.environ.get("APP_INSTANCE_NETWORK_NAME", "nano-instance-edge").strip()
|
||||
DEFAULT_PROVIDER = os.environ.get("APP_INSTANCE_PROVIDER", "openai").strip()
|
||||
DEFAULT_MODEL = os.environ.get("APP_INSTANCE_MODEL", "openai/gpt-5").strip()
|
||||
DEFAULT_API_KEY = os.environ.get("APP_INSTANCE_API_KEY", "").strip()
|
||||
DEFAULT_API_BASE = os.environ.get("APP_INSTANCE_API_BASE", "").strip()
|
||||
INSTANCE_IMAGE = os.environ.get("APP_INSTANCE_IMAGE", "beaver/app-instance:latest").strip()
|
||||
INSTANCE_NETWORK_NAME = os.environ.get("APP_INSTANCE_NETWORK_NAME", "beaver-instance-edge").strip()
|
||||
DEFAULT_AUTHZ_BASE_URL = os.environ.get("DEFAULT_AUTHZ_BASE_URL", "").strip()
|
||||
DEFAULT_AUTHZ_OUTLOOK_MCP_URL = os.environ.get("DEFAULT_AUTHZ_OUTLOOK_MCP_URL", "").strip()
|
||||
DEFAULT_OUTLOOK_MCP_SERVER_ID = os.environ.get("DEFAULT_OUTLOOK_MCP_SERVER_ID", "outlook_mcp").strip() or "outlook_mcp"
|
||||
@ -53,6 +49,23 @@ HEALTH_INTERVAL_SECONDS = float(os.environ.get("DEPLOY_HEALTH_INTERVAL_SECONDS",
|
||||
INSTANCE_INTERNAL_PORT = int(os.environ.get("APP_INSTANCE_INTERNAL_PORT", "8080").strip() or "8080")
|
||||
SERVER_HOST = os.environ.get("DEPLOY_CONTROL_HOST", "0.0.0.0").strip() or "0.0.0.0"
|
||||
SERVER_PORT = int(os.environ.get("DEPLOY_CONTROL_PORT", "8090").strip() or "8090")
|
||||
KNOWN_PROVIDERS = {
|
||||
"anthropic",
|
||||
"openai",
|
||||
"openrouter",
|
||||
"deepseek",
|
||||
"groq",
|
||||
"zhipu",
|
||||
"dashscope",
|
||||
"vllm",
|
||||
"gemini",
|
||||
"moonshot",
|
||||
"minimax",
|
||||
"aihubmix",
|
||||
"siliconflow",
|
||||
"volcengine",
|
||||
}
|
||||
API_KEY_OPTIONAL_PROVIDERS = {"vllm"}
|
||||
|
||||
|
||||
class ApiError(Exception):
|
||||
@ -87,6 +100,13 @@ def run_command(args: list[str], *, cwd: Path | None = None, extra_env: dict[str
|
||||
return completed.stdout.strip()
|
||||
|
||||
|
||||
def write_json_file(path: Path, data: dict[str, Any]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp_path = path.with_name(f"{path.name}.tmp")
|
||||
tmp_path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
||||
tmp_path.replace(path)
|
||||
|
||||
|
||||
def load_registry() -> dict[str, Any]:
|
||||
if not REGISTRY_PATH.exists():
|
||||
return {"instances": []}
|
||||
@ -210,10 +230,6 @@ def create_or_get_instance(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
ensure_network()
|
||||
public_host = build_public_host(slug=slug, instance_id=instance_id, username=username)
|
||||
public_url = build_public_url(public_host)
|
||||
provider = str(payload.get("provider", "") or DEFAULT_PROVIDER).strip() or DEFAULT_PROVIDER
|
||||
model = str(payload.get("model", "") or DEFAULT_MODEL).strip() or DEFAULT_MODEL
|
||||
api_key = str(payload.get("api_key", "") or DEFAULT_API_KEY).strip()
|
||||
api_base = str(payload.get("api_base", "") or DEFAULT_API_BASE).strip()
|
||||
authz_base_url = str(payload.get("authz_base_url", "") or DEFAULT_AUTHZ_BASE_URL).strip()
|
||||
authz_outlook_mcp_url = str(
|
||||
payload.get("authz_outlook_mcp_url", "") or DEFAULT_AUTHZ_OUTLOOK_MCP_URL
|
||||
@ -221,9 +237,6 @@ def create_or_get_instance(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
backend_name = str(payload.get("backend_name", "") or username).strip() or username
|
||||
image_name = str(payload.get("image_name", "") or INSTANCE_IMAGE).strip() or INSTANCE_IMAGE
|
||||
|
||||
if not api_key:
|
||||
raise ApiError(HTTPStatus.BAD_REQUEST, "api key is required for new instances")
|
||||
|
||||
command = [
|
||||
str(CREATE_INSTANCE_SCRIPT),
|
||||
"--image",
|
||||
@ -238,12 +251,7 @@ def create_or_get_instance(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
username,
|
||||
"--email",
|
||||
email,
|
||||
"--provider",
|
||||
provider,
|
||||
"--model",
|
||||
model,
|
||||
"--api-key",
|
||||
api_key,
|
||||
"--skip-provider-config",
|
||||
"--backend-name",
|
||||
backend_name,
|
||||
"--public-url",
|
||||
@ -253,8 +261,6 @@ def create_or_get_instance(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
"--network",
|
||||
INSTANCE_NETWORK_NAME,
|
||||
]
|
||||
if api_base:
|
||||
command.extend(["--api-base", api_base])
|
||||
if authz_base_url:
|
||||
command.extend(["--authz-base-url", authz_base_url])
|
||||
if authz_outlook_mcp_url:
|
||||
@ -282,6 +288,109 @@ def create_or_get_instance(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def configure_instance_provider(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
instance_id = str(payload.get("instance_id", "") or "").strip()
|
||||
username = str(payload.get("username", "") or "").strip()
|
||||
if not instance_id and not username:
|
||||
raise ApiError(HTTPStatus.BAD_REQUEST, "instance_id or username is required")
|
||||
|
||||
record = None
|
||||
if instance_id:
|
||||
record = get_registry_record(instance_id=instance_id)
|
||||
if record is None and username:
|
||||
record = get_registry_record(username=username)
|
||||
if record is None:
|
||||
raise ApiError(HTTPStatus.NOT_FOUND, "instance not found")
|
||||
|
||||
if payload.get("skip") is True:
|
||||
return {
|
||||
"configured": False,
|
||||
"skipped": True,
|
||||
"instance": record,
|
||||
"public_url": str(record.get("public_url", "") or ""),
|
||||
"frontend_base_url": str(record.get("frontend_base_url", "") or record.get("public_url", "") or ""),
|
||||
"api_base_url": build_internal_api_base_url(record),
|
||||
}
|
||||
|
||||
provider = str(payload.get("provider", "") or "").strip()
|
||||
model = str(payload.get("model", "") or "").strip()
|
||||
api_key = str(payload.get("api_key", "") or "").strip()
|
||||
api_base = str(payload.get("api_base", "") or "").strip()
|
||||
|
||||
if provider not in KNOWN_PROVIDERS:
|
||||
raise ApiError(HTTPStatus.BAD_REQUEST, f"unsupported provider: {provider or '(empty)'}")
|
||||
if not model:
|
||||
raise ApiError(HTTPStatus.BAD_REQUEST, "model is required")
|
||||
if provider not in API_KEY_OPTIONAL_PROVIDERS and not api_key:
|
||||
raise ApiError(HTTPStatus.BAD_REQUEST, "api key is required")
|
||||
if provider in API_KEY_OPTIONAL_PROVIDERS and not (api_key or api_base):
|
||||
raise ApiError(HTTPStatus.BAD_REQUEST, "api key or api base is required")
|
||||
|
||||
raw_config_path = str(record.get("config_path", "") or "").strip()
|
||||
config_path = Path(raw_config_path).expanduser()
|
||||
if not raw_config_path:
|
||||
beaver_home = Path(str(record.get("beaver_home", "") or "")).expanduser()
|
||||
config_path = beaver_home / "config.json"
|
||||
if not config_path.is_absolute():
|
||||
config_path = (APP_INSTANCE_DIR / config_path).resolve()
|
||||
if not config_path.exists():
|
||||
raise ApiError(HTTPStatus.NOT_FOUND, f"instance config not found: {config_path}")
|
||||
|
||||
try:
|
||||
raw = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ApiError(HTTPStatus.BAD_GATEWAY, f"invalid instance config: {config_path}") from exc
|
||||
if not isinstance(raw, dict):
|
||||
raise ApiError(HTTPStatus.BAD_GATEWAY, f"instance config must be an object: {config_path}")
|
||||
|
||||
agents = raw.get("agents")
|
||||
if not isinstance(agents, dict):
|
||||
agents = {}
|
||||
raw["agents"] = agents
|
||||
defaults = agents.get("defaults")
|
||||
if not isinstance(defaults, dict):
|
||||
defaults = {}
|
||||
agents["defaults"] = defaults
|
||||
providers = raw.get("providers")
|
||||
if not isinstance(providers, dict):
|
||||
providers = {}
|
||||
raw["providers"] = providers
|
||||
|
||||
provider_payload: dict[str, Any] = {}
|
||||
if api_key:
|
||||
provider_payload["apiKey"] = api_key
|
||||
if api_base:
|
||||
provider_payload["apiBase"] = api_base
|
||||
providers.clear()
|
||||
providers[provider] = provider_payload
|
||||
defaults["workspace"] = str(defaults.get("workspace", "") or "/root/.beaver/workspace")
|
||||
defaults["provider"] = provider
|
||||
defaults["model"] = model
|
||||
|
||||
write_json_file(config_path, raw)
|
||||
|
||||
container_name = str(record.get("container_name", "") or "").strip()
|
||||
if not container_name:
|
||||
raise ApiError(HTTPStatus.BAD_GATEWAY, "instance container name is missing")
|
||||
run_command(["docker", "restart", container_name])
|
||||
wait_for_backend(record)
|
||||
ensure_proxy()
|
||||
|
||||
updated = get_registry_record(instance_id=str(record.get("instance_id", "") or instance_id))
|
||||
if updated is None:
|
||||
updated = record
|
||||
return {
|
||||
"configured": True,
|
||||
"skipped": False,
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
"instance": updated,
|
||||
"public_url": str(updated.get("public_url", "") or ""),
|
||||
"frontend_base_url": str(updated.get("frontend_base_url", "") or updated.get("public_url", "") or ""),
|
||||
"api_base_url": build_internal_api_base_url(updated),
|
||||
}
|
||||
|
||||
|
||||
def _upsert_registry_record(record: dict[str, Any]) -> dict[str, Any]:
|
||||
instance_id = str(record.get("instance_id", "") or "").strip()
|
||||
if not instance_id:
|
||||
@ -467,6 +576,10 @@ class Handler(BaseHTTPRequestHandler):
|
||||
payload = self._read_json_body()
|
||||
self._json_response(HTTPStatus.OK, resolve_instance(payload))
|
||||
return
|
||||
if self.path == "/api/instances/configure-provider":
|
||||
payload = self._read_json_body()
|
||||
self._json_response(HTTPStatus.OK, configure_instance_provider(payload))
|
||||
return
|
||||
raise ApiError(HTTPStatus.NOT_FOUND, "not found")
|
||||
except ApiError as exc:
|
||||
self._json_response(exc.status_code, {"detail": exc.detail})
|
||||
|
||||
Reference in New Issue
Block a user