chore: update external connector deployment flow
This commit is contained in:
@ -148,6 +148,12 @@ BEAVER_WORKSPACE=/root/.beaver/workspace
|
||||
- 实例容器的宿主机端口默认只绑定 `127.0.0.1`
|
||||
- 外部访问应统一走 `router-proxy`
|
||||
- 如果你确实要把单个实例端口直接暴露到公网,再显式传 `--host-bind-ip 0.0.0.0`
|
||||
- 使用共享 `external-connector` sidecar 时,每个实例容器都必须带自己的内部回调地址:
|
||||
`EXTERNAL_CONNECTOR_CALLBACK_BASE_URL=http://<app-instance-container-name>:8080`
|
||||
- 通过 `create-instance.sh --network <docker-network>` 创建实例时,脚本会默认使用
|
||||
`http://<container-name>:8080` 作为回调地址;生产部署也可以用
|
||||
`--external-connector-callback-base-url <url>` 显式覆盖
|
||||
- `BEAVER_BRIDGE_BASE_URL` 只作为 sidecar 的旧连接或兜底地址;多实例部署不能依赖它路由所有入站事件
|
||||
|
||||
下一步可以继续接:
|
||||
|
||||
|
||||
@ -15,7 +15,9 @@ class AgentRegistry:
|
||||
self.path = self.workspace / "agents" / "registry.json"
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not self.path.exists():
|
||||
self._write_agents(_builtin_agents())
|
||||
self._write_agents([])
|
||||
else:
|
||||
self._drop_legacy_builtin_agents()
|
||||
|
||||
def list_agents(self, *, include_disabled: bool = True) -> list[RegisteredAgent]:
|
||||
agents = self._read_agents()
|
||||
@ -125,72 +127,14 @@ class AgentRegistry:
|
||||
payload = {"version": 1, "agents": [agent.to_dict() for agent in agents]}
|
||||
self.path.write_text(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
|
||||
def _drop_legacy_builtin_agents(self) -> None:
|
||||
agents = self._read_agents()
|
||||
migrated = [agent for agent in agents if agent.source != "builtin"]
|
||||
if len(migrated) != len(agents):
|
||||
self._write_agents(migrated)
|
||||
|
||||
|
||||
def _terms(text: str) -> set[str]:
|
||||
normalized = "".join(ch.lower() if ch.isalnum() else " " for ch in text)
|
||||
return {part for part in normalized.split() if part}
|
||||
|
||||
|
||||
def _builtin_agents() -> list[RegisteredAgent]:
|
||||
return [
|
||||
RegisteredAgent(
|
||||
agent_id="researcher",
|
||||
name="researcher",
|
||||
display_name="Researcher",
|
||||
role="research",
|
||||
description="Finds facts, references, constraints, and implementation options.",
|
||||
system_prompt="You are a research specialist. Gather concise evidence and tradeoffs for the parent task.",
|
||||
capabilities=["research", "analysis", "source review", "requirements"],
|
||||
tags=["planning", "research"],
|
||||
priority=50,
|
||||
source="builtin",
|
||||
),
|
||||
RegisteredAgent(
|
||||
agent_id="implementer",
|
||||
name="implementer",
|
||||
display_name="Implementer",
|
||||
role="implementation",
|
||||
description="Builds scoped implementation slices and proposes concrete changes.",
|
||||
system_prompt="You are an implementation specialist. Produce practical, scoped implementation output.",
|
||||
capabilities=["implementation", "coding", "refactor", "integration"],
|
||||
tags=["coding", "build"],
|
||||
priority=45,
|
||||
source="builtin",
|
||||
),
|
||||
RegisteredAgent(
|
||||
agent_id="reviewer",
|
||||
name="reviewer",
|
||||
display_name="Reviewer",
|
||||
role="review",
|
||||
description="Reviews plans, code, outputs, and risks before final synthesis.",
|
||||
system_prompt="You are a review specialist. Focus on defects, missing requirements, and risks.",
|
||||
capabilities=["review", "quality", "risk", "verification"],
|
||||
tags=["review", "quality"],
|
||||
priority=45,
|
||||
source="builtin",
|
||||
),
|
||||
RegisteredAgent(
|
||||
agent_id="tester",
|
||||
name="tester",
|
||||
display_name="Tester",
|
||||
role="testing",
|
||||
description="Designs and executes verification checks for task outputs.",
|
||||
system_prompt="You are a testing specialist. Identify focused checks and report pass/fail evidence.",
|
||||
capabilities=["testing", "verification", "regression", "qa"],
|
||||
tags=["test", "quality"],
|
||||
priority=40,
|
||||
source="builtin",
|
||||
),
|
||||
RegisteredAgent(
|
||||
agent_id="documenter",
|
||||
name="documenter",
|
||||
display_name="Documenter",
|
||||
role="documentation",
|
||||
description="Writes and reconciles user-facing and internal documentation updates.",
|
||||
system_prompt="You are a documentation specialist. Produce concise docs aligned with the implementation.",
|
||||
capabilities=["documentation", "explanation", "migration notes", "release notes"],
|
||||
tags=["docs", "communication"],
|
||||
priority=35,
|
||||
source="builtin",
|
||||
),
|
||||
]
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from .models import ChannelRuntimeSpec, ValidationResult
|
||||
@ -37,6 +38,7 @@ class ExternalConnectorBase:
|
||||
self.credential_store = credential_store
|
||||
self.sidecar_client = sidecar_client
|
||||
self.sidecar_base_url = sidecar_base_url
|
||||
self.callback_base_url = _callback_base_url()
|
||||
|
||||
async def start_session(
|
||||
self,
|
||||
@ -63,7 +65,7 @@ class ExternalConnectorBase:
|
||||
"connectionId": connection.connection_id,
|
||||
"channelId": connection.channel_id,
|
||||
"displayName": connection.display_name,
|
||||
"callbackBaseUrl": "",
|
||||
"callbackBaseUrl": self.callback_base_url,
|
||||
"options": dict(options),
|
||||
}
|
||||
view = dict(await self.sidecar_client.start_session(payload))
|
||||
@ -176,6 +178,14 @@ def _policy_runtime_config(options: dict[str, Any]) -> dict[str, Any]:
|
||||
return result
|
||||
|
||||
|
||||
def _callback_base_url() -> str:
|
||||
for name in ("EXTERNAL_CONNECTOR_CALLBACK_BASE_URL", "BEAVER_CONNECTOR_CALLBACK_BASE_URL"):
|
||||
value = os.environ.get(name, "").strip()
|
||||
if value:
|
||||
return value.rstrip("/")
|
||||
return ""
|
||||
|
||||
|
||||
def _string_list(value: Any) -> list[str]:
|
||||
if isinstance(value, str):
|
||||
raw_items = value.replace("\n", ",").split(",")
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from beaver.coordinator.models import AgentDescriptor, ExecutionGraph, ExecutionNode
|
||||
from beaver.coordinator.registry import AgentRegistry, RegisteredAgent, TargetResolver
|
||||
from beaver.tasks import TaskRecord
|
||||
@ -20,22 +22,64 @@ def _task() -> TaskRecord:
|
||||
)
|
||||
|
||||
|
||||
def test_registry_seeds_builtin_agents_and_filters_disabled(tmp_path) -> None:
|
||||
def test_registry_starts_empty_and_filters_disabled(tmp_path) -> None:
|
||||
registry = AgentRegistry(tmp_path)
|
||||
|
||||
assert {agent.agent_id for agent in registry.list_active_agents()} >= {
|
||||
"researcher",
|
||||
"implementer",
|
||||
"reviewer",
|
||||
"tester",
|
||||
"documenter",
|
||||
}
|
||||
assert registry.list_agents() == []
|
||||
|
||||
registry.upsert_agent(
|
||||
RegisteredAgent(
|
||||
agent_id="tester",
|
||||
name="tester",
|
||||
display_name="Tester",
|
||||
role="testing",
|
||||
description="Runs checks.",
|
||||
system_prompt="test",
|
||||
)
|
||||
)
|
||||
registry.disable_agent("tester")
|
||||
|
||||
assert "tester" not in {agent.agent_id for agent in registry.list_active_agents()}
|
||||
|
||||
|
||||
def test_registry_drops_legacy_builtin_agents(tmp_path) -> None:
|
||||
registry_path = tmp_path / "agents" / "registry.json"
|
||||
registry_path.parent.mkdir(parents=True)
|
||||
registry_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"version": 1,
|
||||
"agents": [
|
||||
{
|
||||
"agent_id": "researcher",
|
||||
"name": "researcher",
|
||||
"display_name": "Researcher",
|
||||
"role": "research",
|
||||
"description": "legacy builtin",
|
||||
"system_prompt": "research",
|
||||
"source": "builtin",
|
||||
},
|
||||
{
|
||||
"agent_id": "workspace-agent",
|
||||
"name": "workspace-agent",
|
||||
"display_name": "Workspace Agent",
|
||||
"role": "workspace",
|
||||
"description": "user configured",
|
||||
"system_prompt": "work",
|
||||
"source": "workspace",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
registry = AgentRegistry(tmp_path)
|
||||
|
||||
assert [agent.agent_id for agent in registry.list_agents()] == ["workspace-agent"]
|
||||
|
||||
|
||||
def test_resolver_selects_registered_agent_by_role_and_capabilities(tmp_path) -> None:
|
||||
registry = AgentRegistry(tmp_path)
|
||||
registry.upsert_agent(
|
||||
@ -88,4 +132,3 @@ def test_resolver_falls_back_to_ephemeral_agent_when_no_match(tmp_path) -> None:
|
||||
assert resolved.nodes[0].agent.name == "rare"
|
||||
assert resolved.nodes[0].agent.metadata["resolution"] == "fallback_ephemeral"
|
||||
assert reports[0].fallback_used is True
|
||||
|
||||
|
||||
@ -59,8 +59,9 @@ class ImmediateConnectedSidecarClient(FakeSidecarClient):
|
||||
return session
|
||||
|
||||
|
||||
def test_weixin_connector_starts_connector_session(tmp_path) -> None:
|
||||
def test_weixin_connector_starts_connector_session(tmp_path, monkeypatch) -> None:
|
||||
async def run() -> None:
|
||||
monkeypatch.setenv("EXTERNAL_CONNECTOR_CALLBACK_BASE_URL", "http://app-instance-jaychen:8080")
|
||||
connection_store = ChannelConnectionStore(tmp_path / "connections.json")
|
||||
credential_store = CredentialStore(tmp_path / "credentials.json")
|
||||
client = FakeSidecarClient()
|
||||
@ -77,6 +78,7 @@ def test_weixin_connector_starts_connector_session(tmp_path) -> None:
|
||||
assert view["connectionId"].startswith("conn_")
|
||||
assert client.started[0]["kind"] == "weixin"
|
||||
assert client.started[0]["connectionId"].startswith("conn_")
|
||||
assert client.started[0]["callbackBaseUrl"] == "http://app-instance-jaychen:8080"
|
||||
assert connection_store.list()[0].kind == "weixin"
|
||||
assert connection_store.list()[0].status == "pairing"
|
||||
|
||||
|
||||
@ -22,6 +22,7 @@ USER_FILES_MAX_UPLOAD_BYTES="${USER_FILES_MAX_UPLOAD_BYTES:-}"
|
||||
EXTERNAL_CONNECTOR_BASE_URL="${EXTERNAL_CONNECTOR_BASE_URL:-http://external-connector:8787}"
|
||||
EXTERNAL_CONNECTOR_TOKEN="${EXTERNAL_CONNECTOR_TOKEN:-}"
|
||||
BEAVER_BRIDGE_TOKEN="${BEAVER_BRIDGE_TOKEN:-}"
|
||||
EXTERNAL_CONNECTOR_CALLBACK_BASE_URL="${EXTERNAL_CONNECTOR_CALLBACK_BASE_URL:-}"
|
||||
BACKEND_ID=""
|
||||
CLIENT_ID=""
|
||||
CLIENT_SECRET=""
|
||||
@ -81,6 +82,9 @@ Optional:
|
||||
External connector sidecar URL. Default: http://external-connector:8787
|
||||
--external-connector-token <token>
|
||||
Service token used for Beaver-to-sidecar requests.
|
||||
--external-connector-callback-base-url <url>
|
||||
Internal URL the sidecar should call back for inbound events.
|
||||
Default: http://<container-name>:8080 when a Docker network is used.
|
||||
--bridge-token <token> Service token accepted from the connector bridge.
|
||||
--backend-id <id> Pre-assigned backend id.
|
||||
--client-id <id> Pre-assigned AuthZ client id.
|
||||
@ -565,6 +569,10 @@ while [[ $# -gt 0 ]]; do
|
||||
EXTERNAL_CONNECTOR_TOKEN="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--external-connector-callback-base-url)
|
||||
EXTERNAL_CONNECTOR_CALLBACK_BASE_URL="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--bridge-token)
|
||||
BEAVER_BRIDGE_TOKEN="${2:-}"
|
||||
shift 2
|
||||
@ -772,6 +780,10 @@ RUN_ARGS=(
|
||||
--label "beaver.instance.public_url=${PUBLIC_URL}"
|
||||
)
|
||||
|
||||
if [[ -z "$EXTERNAL_CONNECTOR_CALLBACK_BASE_URL" && -n "$NETWORK_NAME" ]]; then
|
||||
EXTERNAL_CONNECTOR_CALLBACK_BASE_URL="http://${CONTAINER_NAME}:8080"
|
||||
fi
|
||||
|
||||
if [[ "$SEED_INITIAL_SKILLS" -eq 1 && -n "$INITIAL_SKILLS_DIR" ]]; then
|
||||
RUN_ARGS+=(
|
||||
-v "${INITIAL_SKILLS_DIR}:/opt/app/initial-skills:ro"
|
||||
@ -786,6 +798,9 @@ fi
|
||||
if [[ -n "$EXTERNAL_CONNECTOR_TOKEN" ]]; then
|
||||
RUN_ARGS+=(-e "EXTERNAL_CONNECTOR_TOKEN=${EXTERNAL_CONNECTOR_TOKEN}")
|
||||
fi
|
||||
if [[ -n "$EXTERNAL_CONNECTOR_CALLBACK_BASE_URL" ]]; then
|
||||
RUN_ARGS+=(-e "EXTERNAL_CONNECTOR_CALLBACK_BASE_URL=${EXTERNAL_CONNECTOR_CALLBACK_BASE_URL}")
|
||||
fi
|
||||
if [[ -n "$BEAVER_BRIDGE_TOKEN" ]]; then
|
||||
RUN_ARGS+=(-e "BEAVER_BRIDGE_TOKEN=${BEAVER_BRIDGE_TOKEN}")
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user