chore: update external connector deployment flow
This commit is contained in:
@ -148,6 +148,12 @@ BEAVER_WORKSPACE=/root/.beaver/workspace
|
|||||||
- 实例容器的宿主机端口默认只绑定 `127.0.0.1`
|
- 实例容器的宿主机端口默认只绑定 `127.0.0.1`
|
||||||
- 外部访问应统一走 `router-proxy`
|
- 外部访问应统一走 `router-proxy`
|
||||||
- 如果你确实要把单个实例端口直接暴露到公网,再显式传 `--host-bind-ip 0.0.0.0`
|
- 如果你确实要把单个实例端口直接暴露到公网,再显式传 `--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 = self.workspace / "agents" / "registry.json"
|
||||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
if not self.path.exists():
|
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]:
|
def list_agents(self, *, include_disabled: bool = True) -> list[RegisteredAgent]:
|
||||||
agents = self._read_agents()
|
agents = self._read_agents()
|
||||||
@ -125,72 +127,14 @@ class AgentRegistry:
|
|||||||
payload = {"version": 1, "agents": [agent.to_dict() for agent in agents]}
|
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")
|
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]:
|
def _terms(text: str) -> set[str]:
|
||||||
normalized = "".join(ch.lower() if ch.isalnum() else " " for ch in text)
|
normalized = "".join(ch.lower() if ch.isalnum() else " " for ch in text)
|
||||||
return {part for part in normalized.split() if part}
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from .models import ChannelRuntimeSpec, ValidationResult
|
from .models import ChannelRuntimeSpec, ValidationResult
|
||||||
@ -37,6 +38,7 @@ class ExternalConnectorBase:
|
|||||||
self.credential_store = credential_store
|
self.credential_store = credential_store
|
||||||
self.sidecar_client = sidecar_client
|
self.sidecar_client = sidecar_client
|
||||||
self.sidecar_base_url = sidecar_base_url
|
self.sidecar_base_url = sidecar_base_url
|
||||||
|
self.callback_base_url = _callback_base_url()
|
||||||
|
|
||||||
async def start_session(
|
async def start_session(
|
||||||
self,
|
self,
|
||||||
@ -63,7 +65,7 @@ class ExternalConnectorBase:
|
|||||||
"connectionId": connection.connection_id,
|
"connectionId": connection.connection_id,
|
||||||
"channelId": connection.channel_id,
|
"channelId": connection.channel_id,
|
||||||
"displayName": connection.display_name,
|
"displayName": connection.display_name,
|
||||||
"callbackBaseUrl": "",
|
"callbackBaseUrl": self.callback_base_url,
|
||||||
"options": dict(options),
|
"options": dict(options),
|
||||||
}
|
}
|
||||||
view = dict(await self.sidecar_client.start_session(payload))
|
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
|
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]:
|
def _string_list(value: Any) -> list[str]:
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
raw_items = value.replace("\n", ",").split(",")
|
raw_items = value.replace("\n", ",").split(",")
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
from beaver.coordinator.models import AgentDescriptor, ExecutionGraph, ExecutionNode
|
from beaver.coordinator.models import AgentDescriptor, ExecutionGraph, ExecutionNode
|
||||||
from beaver.coordinator.registry import AgentRegistry, RegisteredAgent, TargetResolver
|
from beaver.coordinator.registry import AgentRegistry, RegisteredAgent, TargetResolver
|
||||||
from beaver.tasks import TaskRecord
|
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)
|
registry = AgentRegistry(tmp_path)
|
||||||
|
|
||||||
assert {agent.agent_id for agent in registry.list_active_agents()} >= {
|
assert registry.list_agents() == []
|
||||||
"researcher",
|
|
||||||
"implementer",
|
|
||||||
"reviewer",
|
|
||||||
"tester",
|
|
||||||
"documenter",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
registry.upsert_agent(
|
||||||
|
RegisteredAgent(
|
||||||
|
agent_id="tester",
|
||||||
|
name="tester",
|
||||||
|
display_name="Tester",
|
||||||
|
role="testing",
|
||||||
|
description="Runs checks.",
|
||||||
|
system_prompt="test",
|
||||||
|
)
|
||||||
|
)
|
||||||
registry.disable_agent("tester")
|
registry.disable_agent("tester")
|
||||||
|
|
||||||
assert "tester" not in {agent.agent_id for agent in registry.list_active_agents()}
|
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:
|
def test_resolver_selects_registered_agent_by_role_and_capabilities(tmp_path) -> None:
|
||||||
registry = AgentRegistry(tmp_path)
|
registry = AgentRegistry(tmp_path)
|
||||||
registry.upsert_agent(
|
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.name == "rare"
|
||||||
assert resolved.nodes[0].agent.metadata["resolution"] == "fallback_ephemeral"
|
assert resolved.nodes[0].agent.metadata["resolution"] == "fallback_ephemeral"
|
||||||
assert reports[0].fallback_used is True
|
assert reports[0].fallback_used is True
|
||||||
|
|
||||||
|
|||||||
@ -59,8 +59,9 @@ class ImmediateConnectedSidecarClient(FakeSidecarClient):
|
|||||||
return session
|
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:
|
async def run() -> None:
|
||||||
|
monkeypatch.setenv("EXTERNAL_CONNECTOR_CALLBACK_BASE_URL", "http://app-instance-jaychen:8080")
|
||||||
connection_store = ChannelConnectionStore(tmp_path / "connections.json")
|
connection_store = ChannelConnectionStore(tmp_path / "connections.json")
|
||||||
credential_store = CredentialStore(tmp_path / "credentials.json")
|
credential_store = CredentialStore(tmp_path / "credentials.json")
|
||||||
client = FakeSidecarClient()
|
client = FakeSidecarClient()
|
||||||
@ -77,6 +78,7 @@ def test_weixin_connector_starts_connector_session(tmp_path) -> None:
|
|||||||
assert view["connectionId"].startswith("conn_")
|
assert view["connectionId"].startswith("conn_")
|
||||||
assert client.started[0]["kind"] == "weixin"
|
assert client.started[0]["kind"] == "weixin"
|
||||||
assert client.started[0]["connectionId"].startswith("conn_")
|
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].kind == "weixin"
|
||||||
assert connection_store.list()[0].status == "pairing"
|
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_BASE_URL="${EXTERNAL_CONNECTOR_BASE_URL:-http://external-connector:8787}"
|
||||||
EXTERNAL_CONNECTOR_TOKEN="${EXTERNAL_CONNECTOR_TOKEN:-}"
|
EXTERNAL_CONNECTOR_TOKEN="${EXTERNAL_CONNECTOR_TOKEN:-}"
|
||||||
BEAVER_BRIDGE_TOKEN="${BEAVER_BRIDGE_TOKEN:-}"
|
BEAVER_BRIDGE_TOKEN="${BEAVER_BRIDGE_TOKEN:-}"
|
||||||
|
EXTERNAL_CONNECTOR_CALLBACK_BASE_URL="${EXTERNAL_CONNECTOR_CALLBACK_BASE_URL:-}"
|
||||||
BACKEND_ID=""
|
BACKEND_ID=""
|
||||||
CLIENT_ID=""
|
CLIENT_ID=""
|
||||||
CLIENT_SECRET=""
|
CLIENT_SECRET=""
|
||||||
@ -81,6 +82,9 @@ Optional:
|
|||||||
External connector sidecar URL. Default: http://external-connector:8787
|
External connector sidecar URL. Default: http://external-connector:8787
|
||||||
--external-connector-token <token>
|
--external-connector-token <token>
|
||||||
Service token used for Beaver-to-sidecar requests.
|
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.
|
--bridge-token <token> Service token accepted from the connector bridge.
|
||||||
--backend-id <id> Pre-assigned backend id.
|
--backend-id <id> Pre-assigned backend id.
|
||||||
--client-id <id> Pre-assigned AuthZ client id.
|
--client-id <id> Pre-assigned AuthZ client id.
|
||||||
@ -565,6 +569,10 @@ while [[ $# -gt 0 ]]; do
|
|||||||
EXTERNAL_CONNECTOR_TOKEN="${2:-}"
|
EXTERNAL_CONNECTOR_TOKEN="${2:-}"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
--external-connector-callback-base-url)
|
||||||
|
EXTERNAL_CONNECTOR_CALLBACK_BASE_URL="${2:-}"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
--bridge-token)
|
--bridge-token)
|
||||||
BEAVER_BRIDGE_TOKEN="${2:-}"
|
BEAVER_BRIDGE_TOKEN="${2:-}"
|
||||||
shift 2
|
shift 2
|
||||||
@ -772,6 +780,10 @@ RUN_ARGS=(
|
|||||||
--label "beaver.instance.public_url=${PUBLIC_URL}"
|
--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
|
if [[ "$SEED_INITIAL_SKILLS" -eq 1 && -n "$INITIAL_SKILLS_DIR" ]]; then
|
||||||
RUN_ARGS+=(
|
RUN_ARGS+=(
|
||||||
-v "${INITIAL_SKILLS_DIR}:/opt/app/initial-skills:ro"
|
-v "${INITIAL_SKILLS_DIR}:/opt/app/initial-skills:ro"
|
||||||
@ -786,6 +798,9 @@ fi
|
|||||||
if [[ -n "$EXTERNAL_CONNECTOR_TOKEN" ]]; then
|
if [[ -n "$EXTERNAL_CONNECTOR_TOKEN" ]]; then
|
||||||
RUN_ARGS+=(-e "EXTERNAL_CONNECTOR_TOKEN=${EXTERNAL_CONNECTOR_TOKEN}")
|
RUN_ARGS+=(-e "EXTERNAL_CONNECTOR_TOKEN=${EXTERNAL_CONNECTOR_TOKEN}")
|
||||||
fi
|
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
|
if [[ -n "$BEAVER_BRIDGE_TOKEN" ]]; then
|
||||||
RUN_ARGS+=(-e "BEAVER_BRIDGE_TOKEN=${BEAVER_BRIDGE_TOKEN}")
|
RUN_ARGS+=(-e "BEAVER_BRIDGE_TOKEN=${BEAVER_BRIDGE_TOKEN}")
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -698,9 +698,11 @@ For `terminaltest`, ensure the app container has:
|
|||||||
EXTERNAL_CONNECTOR_BASE_URL=http://external-connector:8787
|
EXTERNAL_CONNECTOR_BASE_URL=http://external-connector:8787
|
||||||
EXTERNAL_CONNECTOR_TOKEN=dev-token
|
EXTERNAL_CONNECTOR_TOKEN=dev-token
|
||||||
BEAVER_BRIDGE_TOKEN=dev-token
|
BEAVER_BRIDGE_TOKEN=dev-token
|
||||||
|
EXTERNAL_CONNECTOR_CALLBACK_BASE_URL=http://app-instance-terminaltest:8080
|
||||||
```
|
```
|
||||||
|
|
||||||
Then recreate the instance with the deployment script used by this repo. Do not mount `/var/run/docker.sock` into Beaver.
|
Then recreate the instance with the deployment script used by this repo. Do not mount `/var/run/docker.sock` into Beaver.
|
||||||
|
For multi-instance deployments, this callback URL must point at the specific app-instance container that owns the connection; the shared sidecar stores it per connector session and uses it for inbound events.
|
||||||
|
|
||||||
- [ ] **Step 6: Manual fake-provider onboarding**
|
- [ ] **Step 6: Manual fake-provider onboarding**
|
||||||
|
|
||||||
|
|||||||
@ -83,6 +83,7 @@ class FeishuBotProvider:
|
|||||||
metadata = {
|
metadata = {
|
||||||
"eventCallbackPath": "/feishu/events",
|
"eventCallbackPath": "/feishu/events",
|
||||||
"eventCallbackUrl": f"{self.public_base_url}/feishu/events",
|
"eventCallbackUrl": f"{self.public_base_url}/feishu/events",
|
||||||
|
"bridgeBaseUrl": str(payload.get("callbackBaseUrl") or self.bridge_base_url),
|
||||||
}
|
}
|
||||||
app_id = str(options.get("appId") or options.get("app_id") or "").strip()
|
app_id = str(options.get("appId") or options.get("app_id") or "").strip()
|
||||||
app_secret = str(options.get("appSecret") or options.get("app_secret") or "").strip()
|
app_secret = str(options.get("appSecret") or options.get("app_secret") or "").strip()
|
||||||
@ -212,7 +213,7 @@ class FeishuBotProvider:
|
|||||||
if bridge_event is None:
|
if bridge_event is None:
|
||||||
return {"ok": True, "ignored": "empty_or_oversized"}
|
return {"ok": True, "ignored": "empty_or_oversized"}
|
||||||
self.bridge_post(
|
self.bridge_post(
|
||||||
f"{self.bridge_base_url}/api/channel-connector-bridge/events",
|
f"{_bridge_base_url(session, self.bridge_base_url)}/api/channel-connector-bridge/events",
|
||||||
bridge_event,
|
bridge_event,
|
||||||
{"Authorization": f"Bearer {self.bridge_token}"},
|
{"Authorization": f"Bearer {self.bridge_token}"},
|
||||||
)
|
)
|
||||||
@ -452,13 +453,17 @@ class FeishuBotProvider:
|
|||||||
"FEISHU_TEXT_BATCH_DELAY_MS": str(_positive_int(metadata.get("textBatchDelayMs"), default=0)),
|
"FEISHU_TEXT_BATCH_DELAY_MS": str(_positive_int(metadata.get("textBatchDelayMs"), default=0)),
|
||||||
"FEISHU_TEXT_BATCH_MAX_MESSAGES": str(_positive_int(metadata.get("textBatchMaxMessages"), default=10)),
|
"FEISHU_TEXT_BATCH_MAX_MESSAGES": str(_positive_int(metadata.get("textBatchMaxMessages"), default=10)),
|
||||||
"FEISHU_TEXT_BATCH_MAX_CHARS": str(_positive_int(metadata.get("textBatchMaxChars"), default=20000)),
|
"FEISHU_TEXT_BATCH_MAX_CHARS": str(_positive_int(metadata.get("textBatchMaxChars"), default=20000)),
|
||||||
"BEAVER_BRIDGE_BASE_URL": self.bridge_base_url,
|
"BEAVER_BRIDGE_BASE_URL": _bridge_base_url(session, self.bridge_base_url),
|
||||||
"BEAVER_BRIDGE_TOKEN": self.bridge_token,
|
"BEAVER_BRIDGE_TOKEN": self.bridge_token,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return subprocess.Popen(["node", str(script)], env=env, cwd=str(script.parent))
|
return subprocess.Popen(["node", str(script)], env=env, cwd=str(script.parent))
|
||||||
|
|
||||||
|
|
||||||
|
def _bridge_base_url(session: ConnectorSessionState, fallback: str) -> str:
|
||||||
|
return str(session.metadata.get("bridgeBaseUrl") or fallback).rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
def _bridge_event_from_feishu(session: ConnectorSessionState, header: dict[str, Any], event: dict[str, Any]) -> dict[str, Any] | None:
|
def _bridge_event_from_feishu(session: ConnectorSessionState, header: dict[str, Any], event: dict[str, Any]) -> dict[str, Any] | None:
|
||||||
message = dict(event.get("message") or {})
|
message = dict(event.get("message") or {})
|
||||||
sender = dict(event.get("sender") or {})
|
sender = dict(event.get("sender") or {})
|
||||||
|
|||||||
@ -246,7 +246,7 @@ class WeixinIlinkProvider:
|
|||||||
flush=True,
|
flush=True,
|
||||||
)
|
)
|
||||||
self.bridge_post(
|
self.bridge_post(
|
||||||
f"{self.bridge_base_url}/api/channel-connector-bridge/events",
|
f"{_bridge_base_url(session, self.bridge_base_url)}/api/channel-connector-bridge/events",
|
||||||
event,
|
event,
|
||||||
{"Authorization": f"Bearer {self.bridge_token}"},
|
{"Authorization": f"Bearer {self.bridge_token}"},
|
||||||
)
|
)
|
||||||
@ -384,6 +384,10 @@ def _url(base_url: str, endpoint: str) -> str:
|
|||||||
return f"{base_url.rstrip('/')}/{endpoint.lstrip('/')}"
|
return f"{base_url.rstrip('/')}/{endpoint.lstrip('/')}"
|
||||||
|
|
||||||
|
|
||||||
|
def _bridge_base_url(session: ConnectorSessionState, fallback: str) -> str:
|
||||||
|
return str(session.metadata.get("bridgeBaseUrl") or fallback).rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
def _base_info() -> dict[str, str]:
|
def _base_info() -> dict[str, str]:
|
||||||
return {"channel_version": "2.4.3", "bot_agent": "Beaver/1.0"}
|
return {"channel_version": "2.4.3", "bot_agent": "Beaver/1.0"}
|
||||||
|
|
||||||
|
|||||||
@ -384,6 +384,44 @@ def test_feishu_event_route_forwards_message_to_bridge(tmp_path) -> None:
|
|||||||
assert bridge_posts[0][1]["peerId"] == "ou_user"
|
assert bridge_posts[0][1]["peerId"] == "ou_user"
|
||||||
|
|
||||||
|
|
||||||
|
def test_feishu_event_route_uses_session_callback_base_url(tmp_path) -> None:
|
||||||
|
bridge_posts: list[tuple[str, dict[str, object], dict[str, str]]] = []
|
||||||
|
provider = _provider(tmp_path, bridge_posts=bridge_posts)
|
||||||
|
provider.start_session(
|
||||||
|
{
|
||||||
|
"kind": "feishu",
|
||||||
|
"connectionId": "conn_1",
|
||||||
|
"channelId": "feishu-main",
|
||||||
|
"displayName": "Feishu Main",
|
||||||
|
"callbackBaseUrl": "http://app-instance-jaychen:8080",
|
||||||
|
"options": {"appId": "cli_xxx", "appSecret": "secret", "verificationToken": "verify-token"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
app = create_app(provider=provider, api_token="sidecar-token")
|
||||||
|
|
||||||
|
with TestClient(app) as client:
|
||||||
|
response = client.post(
|
||||||
|
"/feishu/events",
|
||||||
|
json={
|
||||||
|
"schema": "2.0",
|
||||||
|
"header": {"event_id": "evt_1", "token": "verify-token", "app_id": "cli_xxx"},
|
||||||
|
"event": {
|
||||||
|
"sender": {"sender_id": {"open_id": "ou_user"}},
|
||||||
|
"message": {
|
||||||
|
"message_id": "om_1",
|
||||||
|
"chat_id": "oc_chat",
|
||||||
|
"chat_type": "p2p",
|
||||||
|
"message_type": "text",
|
||||||
|
"content": "{\"text\":\"hello feishu\"}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert bridge_posts[0][0] == "http://app-instance-jaychen:8080/api/channel-connector-bridge/events"
|
||||||
|
|
||||||
|
|
||||||
def test_feishu_event_route_ignores_bot_sender_and_platform_commands(tmp_path) -> None:
|
def test_feishu_event_route_ignores_bot_sender_and_platform_commands(tmp_path) -> None:
|
||||||
bridge_posts: list[tuple[str, dict[str, object], dict[str, str]]] = []
|
bridge_posts: list[tuple[str, dict[str, object], dict[str, str]]] = []
|
||||||
provider = _provider(tmp_path, bridge_posts=bridge_posts)
|
provider = _provider(tmp_path, bridge_posts=bridge_posts)
|
||||||
|
|||||||
@ -438,3 +438,35 @@ def test_weixin_ilink_provider_poll_once_forwards_bridge_event(tmp_path) -> None
|
|||||||
assert bridge_posts[0][1]["eventId"] == "weixin-main:42"
|
assert bridge_posts[0][1]["eventId"] == "weixin-main:42"
|
||||||
assert bridge_posts[0][1]["content"] == "hello"
|
assert bridge_posts[0][1]["content"] == "hello"
|
||||||
assert bridge_posts[0][1]["peerId"] == "wx-user"
|
assert bridge_posts[0][1]["peerId"] == "wx-user"
|
||||||
|
|
||||||
|
|
||||||
|
def test_weixin_ilink_provider_poll_once_uses_session_callback_base_url(tmp_path) -> None:
|
||||||
|
http = FakeHttpClient()
|
||||||
|
bridge_posts: list[tuple[str, dict[str, object], dict[str, str]]] = []
|
||||||
|
|
||||||
|
def bridge_post(url: str, payload: dict[str, object], headers: dict[str, str]) -> None:
|
||||||
|
bridge_posts.append((url, payload, headers))
|
||||||
|
|
||||||
|
provider = WeixinIlinkProvider(
|
||||||
|
store=SidecarStateStore(tmp_path / "state.json"),
|
||||||
|
http_client=http,
|
||||||
|
bridge_base_url="http://global-beaver:8080",
|
||||||
|
bridge_token="bridge-token",
|
||||||
|
bridge_post=bridge_post,
|
||||||
|
start_receivers=False,
|
||||||
|
)
|
||||||
|
session = provider.start_session(
|
||||||
|
{
|
||||||
|
"kind": "weixin",
|
||||||
|
"connectionId": "conn_1",
|
||||||
|
"channelId": "weixin-main",
|
||||||
|
"displayName": "Weixin Main",
|
||||||
|
"callbackBaseUrl": "http://app-instance-jaychen:8080",
|
||||||
|
"options": {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
provider.get_session(session["sessionId"])
|
||||||
|
|
||||||
|
provider.poll_once("conn_1")
|
||||||
|
|
||||||
|
assert bridge_posts[0][0] == "http://app-instance-jaychen:8080/api/channel-connector-bridge/events"
|
||||||
|
|||||||
152
部署指南.md
152
部署指南.md
@ -6,6 +6,7 @@
|
|||||||
- `authz-service`
|
- `authz-service`
|
||||||
- `deploy-control`
|
- `deploy-control`
|
||||||
- `router-proxy`
|
- `router-proxy`
|
||||||
|
- 可选的 `external-connector` sidecar
|
||||||
- 自动创建出来的 `app-instance`
|
- 自动创建出来的 `app-instance`
|
||||||
|
|
||||||
目标结果:
|
目标结果:
|
||||||
@ -35,6 +36,7 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker --version
|
docker --version
|
||||||
|
docker compose version
|
||||||
docker ps
|
docker ps
|
||||||
python3 --version
|
python3 --version
|
||||||
openssl version
|
openssl version
|
||||||
@ -96,6 +98,20 @@ export BEAVER_USER_FILES_MAX_UPLOAD_BYTES=$((5 * 1024 * 1024 * 1024))
|
|||||||
|
|
||||||
export BEAVER_OUTLOOK_MCP_URL=''
|
export BEAVER_OUTLOOK_MCP_URL=''
|
||||||
export BEAVER_OUTLOOK_MCP_SERVER_ID='outlook_mcp'
|
export BEAVER_OUTLOOK_MCP_SERVER_ID='outlook_mcp'
|
||||||
|
|
||||||
|
export EXTERNAL_CONNECTOR_BASE_URL='http://external-connector:8787'
|
||||||
|
export EXTERNAL_CONNECTOR_TOKEN="$(openssl rand -hex 32)"
|
||||||
|
export BEAVER_BRIDGE_TOKEN="$(openssl rand -hex 32)"
|
||||||
|
export EXTERNAL_CONNECTOR_PORT=8787
|
||||||
|
export CONNECTOR_PUBLIC_BASE_URL='http://127.0.0.1:8787'
|
||||||
|
export CONNECTOR_PROVIDER=fake
|
||||||
|
export CONNECTOR_COMMAND_TIMEOUT_SECONDS=120
|
||||||
|
export WEIXIN_CONNECT_COMMAND=''
|
||||||
|
export WEIXIN_STATUS_COMMAND=''
|
||||||
|
export WEIXIN_SEND_COMMAND=''
|
||||||
|
export FEISHU_CONNECT_COMMAND=''
|
||||||
|
export FEISHU_STATUS_COMMAND=''
|
||||||
|
export FEISHU_SEND_COMMAND=''
|
||||||
```
|
```
|
||||||
|
|
||||||
变量说明:
|
变量说明:
|
||||||
@ -115,6 +131,13 @@ export BEAVER_OUTLOOK_MCP_SERVER_ID='outlook_mcp'
|
|||||||
| `BEAVER_USER_FILES_MAX_UPLOAD_BYTES` | 用户文件系统上传上限,默认 5GB;聊天附件和 workspace 上传仍保留当前小文件限制 |
|
| `BEAVER_USER_FILES_MAX_UPLOAD_BYTES` | 用户文件系统上传上限,默认 5GB;聊天附件和 workspace 上传仍保留当前小文件限制 |
|
||||||
| `BEAVER_OUTLOOK_MCP_URL` | 可选 Outlook MCP HTTP 地址 |
|
| `BEAVER_OUTLOOK_MCP_URL` | 可选 Outlook MCP HTTP 地址 |
|
||||||
| `BEAVER_OUTLOOK_MCP_SERVER_ID` | Outlook MCP server id,默认 `outlook_mcp` |
|
| `BEAVER_OUTLOOK_MCP_SERVER_ID` | Outlook MCP server id,默认 `outlook_mcp` |
|
||||||
|
| `EXTERNAL_CONNECTOR_BASE_URL` | app-instance 容器访问外部连接器 sidecar 的地址 |
|
||||||
|
| `EXTERNAL_CONNECTOR_TOKEN` | app-instance 调用 sidecar 管理 API 的 bearer token |
|
||||||
|
| `BEAVER_BRIDGE_TOKEN` | sidecar 回调 app-instance bridge API 的 bearer token |
|
||||||
|
| `EXTERNAL_CONNECTOR_PORT` | sidecar 映射到宿主机的调试端口,默认 `8787` |
|
||||||
|
| `CONNECTOR_PUBLIC_BASE_URL` | sidecar 对外展示自身回调或资源地址时使用的 URL |
|
||||||
|
| `CONNECTOR_PROVIDER` | sidecar provider;本机连通性测试用 `fake`,真实接入再改成 `weixin_ilink`、`feishu_bot` 或 `vendor_cli` |
|
||||||
|
| `WEIXIN_*_COMMAND` / `FEISHU_*_COMMAND` | `vendor_cli` 模式下调用厂商脚本的命令;`fake` 模式留空 |
|
||||||
|
|
||||||
如果接入外部正式 MinIO,不需要启动本地 `beaver-minio`。把上面的 MinIO 变量改成正式服务即可:
|
如果接入外部正式 MinIO,不需要启动本地 `beaver-minio`。把上面的 MinIO 变量改成正式服务即可:
|
||||||
|
|
||||||
@ -212,6 +235,7 @@ docker build -t beaver/app-instance:latest app-instance
|
|||||||
docker build -t beaver/authz-service:latest authz-service
|
docker build -t beaver/authz-service:latest authz-service
|
||||||
docker build -t beaver/deploy-control:latest deploy-control
|
docker build -t beaver/deploy-control:latest deploy-control
|
||||||
docker build -t beaver/auth-portal:latest auth-portal/src
|
docker build -t beaver/auth-portal:latest auth-portal/src
|
||||||
|
docker compose -f docker-compose.external-connectors.yml build external-connector
|
||||||
```
|
```
|
||||||
|
|
||||||
如果某个镜像构建失败,先修构建错误,不要继续往下跑。
|
如果某个镜像构建失败,先修构建错误,不要继续往下跑。
|
||||||
@ -258,7 +282,37 @@ http://alice.localhost:8088
|
|||||||
http://alice.apps.example.com:8088
|
http://alice.apps.example.com:8088
|
||||||
```
|
```
|
||||||
|
|
||||||
## 7. 启动 MinIO
|
## 7. 启动 external-connector sidecar(可选)
|
||||||
|
|
||||||
|
`external-connector` 用于微信、飞书/Lark 这类需要独立进程或厂商 SDK 的连接器。当前部署可以先用 `fake` provider 验证 sidecar、token、网络和 app-instance 回调链路;正式接入时再把 `CONNECTOR_PROVIDER` 和对应命令换成真实配置。
|
||||||
|
|
||||||
|
如果暂时不需要微信或飞书连接器,可以跳过本节。但建议至少在测试环境跑一次,确认部署变量没有断。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
|
||||||
|
docker compose -f docker-compose.external-connectors.yml up -d external-connector
|
||||||
|
```
|
||||||
|
|
||||||
|
检查:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | grep external-connector
|
||||||
|
curl -sS -H "Authorization: Bearer $EXTERNAL_CONNECTOR_TOKEN" \
|
||||||
|
"http://127.0.0.1:${EXTERNAL_CONNECTOR_PORT}/connectors"
|
||||||
|
```
|
||||||
|
|
||||||
|
预期 `/connectors` 返回连接器列表,至少包含 `weixin` 和 `feishu`。如果报 `401`,检查 `EXTERNAL_CONNECTOR_TOKEN` 是否和容器环境变量一致。
|
||||||
|
|
||||||
|
多实例部署时不要依赖 `BEAVER_BRIDGE_BASE_URL=http://app-instance:8080` 这种固定兜底地址。`deploy-control` 通过 `create-instance.sh --network "$BEAVER_NET"` 创建实例时,会让每个 app-instance 默认带自己的回调地址:
|
||||||
|
|
||||||
|
```text
|
||||||
|
EXTERNAL_CONNECTOR_CALLBACK_BASE_URL=http://<app-instance-container-name>:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
sidecar 会按每个连接 session 保存这个回调地址,入站消息才能回到正确的用户实例。
|
||||||
|
|
||||||
|
## 8. 启动 MinIO
|
||||||
|
|
||||||
MinIO 是用户文件系统的后端实现细节。用户和前端不会看到 bucket、access key 或 prefix;Beaver 只通过 `/api/user-files/*` 暴露个人智能体文件系统。
|
MinIO 是用户文件系统的后端实现细节。用户和前端不会看到 bucket、access key 或 prefix;Beaver 只通过 `/api/user-files/*` 暴露个人智能体文件系统。
|
||||||
|
|
||||||
@ -293,7 +347,7 @@ example object: users/alice/uploads/report.pdf
|
|||||||
|
|
||||||
用户文件上传由 Beaver 后端代理到 MinIO,不暴露 bucket、prefix 或凭据。当前默认允许最大 5GB 的用户文件上传,业务上限由 app-instance 后端环境变量 `BEAVER_USER_FILES_MAX_UPLOAD_BYTES` 控制;反向代理默认 `client_max_body_size` 已提高到 5GB。MinIO 本身支持大对象和 multipart 上传,但 agent 对超大文件的读取/处理能力仍需要按具体任务另行验证。
|
用户文件上传由 Beaver 后端代理到 MinIO,不暴露 bucket、prefix 或凭据。当前默认允许最大 5GB 的用户文件上传,业务上限由 app-instance 后端环境变量 `BEAVER_USER_FILES_MAX_UPLOAD_BYTES` 控制;反向代理默认 `client_max_body_size` 已提高到 5GB。MinIO 本身支持大对象和 multipart 上传,但 agent 对超大文件的读取/处理能力仍需要按具体任务另行验证。
|
||||||
|
|
||||||
## 8. 启动 authz-service
|
## 9. 启动 authz-service
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker rm -f beaver-authz-service >/dev/null 2>&1 || true
|
docker rm -f beaver-authz-service >/dev/null 2>&1 || true
|
||||||
@ -332,7 +386,7 @@ docker inspect beaver-authz-service --format '{{range .Config.Env}}{{println .}}
|
|||||||
| egrep '^(AUTHZ_ISSUER|DEPLOY_API_BASE_URL|USER_FILES_MINIO_)='
|
| egrep '^(AUTHZ_ISSUER|DEPLOY_API_BASE_URL|USER_FILES_MINIO_)='
|
||||||
```
|
```
|
||||||
|
|
||||||
## 9. 启动 deploy-control
|
## 10. 启动 deploy-control
|
||||||
|
|
||||||
`deploy-control` 会挂载 Docker socket,再创建新的 `app-instance` 容器。这里最容易错的是路径挂载:
|
`deploy-control` 会挂载 Docker socket,再创建新的 `app-instance` 容器。这里最容易错的是路径挂载:
|
||||||
|
|
||||||
@ -353,8 +407,10 @@ docker run -d \
|
|||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
-v "$PROJECT_ROOT/app-instance:$PROJECT_ROOT/app-instance" \
|
-v "$PROJECT_ROOT/app-instance:$PROJECT_ROOT/app-instance" \
|
||||||
-v "$PROJECT_ROOT/router-proxy:$PROJECT_ROOT/router-proxy" \
|
-v "$PROJECT_ROOT/router-proxy:$PROJECT_ROOT/router-proxy" \
|
||||||
|
-v "$PROJECT_ROOT/skills:$PROJECT_ROOT/skills:ro" \
|
||||||
-e APP_INSTANCE_DIR="$PROJECT_ROOT/app-instance" \
|
-e APP_INSTANCE_DIR="$PROJECT_ROOT/app-instance" \
|
||||||
-e ROUTER_PROXY_DIR="$PROJECT_ROOT/router-proxy" \
|
-e ROUTER_PROXY_DIR="$PROJECT_ROOT/router-proxy" \
|
||||||
|
-e DEFAULT_INITIAL_SKILLS_DIR="$PROJECT_ROOT/skills" \
|
||||||
-e PROXY_CONTAINER_NAME="$BEAVER_PROXY_CONTAINER_NAME" \
|
-e PROXY_CONTAINER_NAME="$BEAVER_PROXY_CONTAINER_NAME" \
|
||||||
-e PROXY_NETWORK_NAME="$BEAVER_NET" \
|
-e PROXY_NETWORK_NAME="$BEAVER_NET" \
|
||||||
-e DEPLOY_CONTROL_API_TOKEN="$BEAVER_DEPLOY_TOKEN" \
|
-e DEPLOY_CONTROL_API_TOKEN="$BEAVER_DEPLOY_TOKEN" \
|
||||||
@ -365,6 +421,9 @@ docker run -d \
|
|||||||
-e DEFAULT_AUTHZ_OUTLOOK_MCP_URL="$BEAVER_OUTLOOK_MCP_URL" \
|
-e DEFAULT_AUTHZ_OUTLOOK_MCP_URL="$BEAVER_OUTLOOK_MCP_URL" \
|
||||||
-e DEFAULT_OUTLOOK_MCP_SERVER_ID="$BEAVER_OUTLOOK_MCP_SERVER_ID" \
|
-e DEFAULT_OUTLOOK_MCP_SERVER_ID="$BEAVER_OUTLOOK_MCP_SERVER_ID" \
|
||||||
-e DEFAULT_USER_FILES_MAX_UPLOAD_BYTES="$BEAVER_USER_FILES_MAX_UPLOAD_BYTES" \
|
-e DEFAULT_USER_FILES_MAX_UPLOAD_BYTES="$BEAVER_USER_FILES_MAX_UPLOAD_BYTES" \
|
||||||
|
-e DEFAULT_EXTERNAL_CONNECTOR_BASE_URL="$EXTERNAL_CONNECTOR_BASE_URL" \
|
||||||
|
-e DEFAULT_EXTERNAL_CONNECTOR_TOKEN="$EXTERNAL_CONNECTOR_TOKEN" \
|
||||||
|
-e DEFAULT_BEAVER_BRIDGE_TOKEN="$BEAVER_BRIDGE_TOKEN" \
|
||||||
-e DEPLOY_PUBLIC_SCHEME="http" \
|
-e DEPLOY_PUBLIC_SCHEME="http" \
|
||||||
-e DEPLOY_PUBLIC_BASE_DOMAIN="$BEAVER_BASE_DOMAIN" \
|
-e DEPLOY_PUBLIC_BASE_DOMAIN="$BEAVER_BASE_DOMAIN" \
|
||||||
-e DEPLOY_PUBLIC_PORT="8088" \
|
-e DEPLOY_PUBLIC_PORT="8088" \
|
||||||
@ -378,7 +437,11 @@ docker run -d \
|
|||||||
|
|
||||||
`DEFAULT_AUTHZ_INTERNAL_TOKEN` 会写入新建 app-instance 的后端 runtime env,用于 app-instance 后端读取自己的 internal MinIO settings。它不会传给前端。
|
`DEFAULT_AUTHZ_INTERNAL_TOKEN` 会写入新建 app-instance 的后端 runtime env,用于 app-instance 后端读取自己的 internal MinIO settings。它不会传给前端。
|
||||||
|
|
||||||
## 10. 启动 auth-portal
|
`DEFAULT_EXTERNAL_CONNECTOR_*` 会写入之后新创建的 app-instance 容器环境变量。改动这些变量后,要重启 `beaver-deploy-control` 并重新创建实例,或手工重建已有实例容器;仅重启 sidecar 不会更新已存在 app-instance 的环境变量。
|
||||||
|
|
||||||
|
`DEFAULT_INITIAL_SKILLS_DIR` 需要和 `skills/` 的只读挂载路径一致。否则新实例能启动,但 workspace 里不会自动种入初始 published skills。
|
||||||
|
|
||||||
|
## 11. 启动 auth-portal
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker rm -f beaver-auth-portal >/dev/null 2>&1 || true
|
docker rm -f beaver-auth-portal >/dev/null 2>&1 || true
|
||||||
@ -401,13 +464,15 @@ docker inspect beaver-auth-portal --format '{{range .Config.Env}}{{println .}}{{
|
|||||||
| egrep '^(AUTHZ_API_BASE_URL|DEPLOY_API_BASE_URL)='
|
| egrep '^(AUTHZ_API_BASE_URL|DEPLOY_API_BASE_URL)='
|
||||||
```
|
```
|
||||||
|
|
||||||
## 11. 健康检查
|
## 12. 健康检查
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl http://127.0.0.1:19090/healthz
|
curl http://127.0.0.1:19090/healthz
|
||||||
curl http://127.0.0.1:8090/healthz
|
curl http://127.0.0.1:8090/healthz
|
||||||
curl -I http://127.0.0.1:3081
|
curl -I http://127.0.0.1:3081
|
||||||
curl -I http://127.0.0.1:9001
|
curl -I http://127.0.0.1:9001
|
||||||
|
curl -sS -H "Authorization: Bearer $EXTERNAL_CONNECTOR_TOKEN" \
|
||||||
|
"http://127.0.0.1:${EXTERNAL_CONNECTOR_PORT}/connectors"
|
||||||
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
|
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
|
||||||
docker logs --tail=50 beaver-router-proxy
|
docker logs --tail=50 beaver-router-proxy
|
||||||
```
|
```
|
||||||
@ -419,8 +484,9 @@ docker logs --tail=50 beaver-router-proxy
|
|||||||
- `beaver-deploy-control`
|
- `beaver-deploy-control`
|
||||||
- `beaver-auth-portal`
|
- `beaver-auth-portal`
|
||||||
- `beaver-router-proxy`
|
- `beaver-router-proxy`
|
||||||
|
- `external-connector`(如果启用了连接器 sidecar)
|
||||||
|
|
||||||
## 12. 浏览器首次测试
|
## 13. 浏览器首次测试
|
||||||
|
|
||||||
打开:
|
打开:
|
||||||
|
|
||||||
@ -572,7 +638,7 @@ docker run --rm --network "$BEAVER_NET" --entrypoint /bin/sh minio/mc:latest -lc
|
|||||||
curl -X DELETE "http://127.0.0.1:19090/backends/$BACKEND_ID/settings/minio"
|
curl -X DELETE "http://127.0.0.1:19090/backends/$BACKEND_ID/settings/minio"
|
||||||
```
|
```
|
||||||
|
|
||||||
## 13. 确认实例已创建
|
## 14. 确认实例已创建
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd "$PROJECT_ROOT/app-instance"
|
cd "$PROJECT_ROOT/app-instance"
|
||||||
@ -590,7 +656,22 @@ docker ps --format 'table {{.Names}}\t{{.Status}}' | grep app-instance
|
|||||||
- `public_url`
|
- `public_url`
|
||||||
- `instance_host`
|
- `instance_host`
|
||||||
|
|
||||||
## 14. 只看 auth-portal 页面
|
确认新实例拿到了连接器环境变量:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
INSTANCE_CONTAINER='<app-instance-container-name>'
|
||||||
|
|
||||||
|
docker inspect "$INSTANCE_CONTAINER" --format '{{range .Config.Env}}{{println .}}{{end}}' \
|
||||||
|
| egrep '^(EXTERNAL_CONNECTOR_BASE_URL|EXTERNAL_CONNECTOR_TOKEN|EXTERNAL_CONNECTOR_CALLBACK_BASE_URL|BEAVER_BRIDGE_TOKEN)='
|
||||||
|
```
|
||||||
|
|
||||||
|
其中 `EXTERNAL_CONNECTOR_CALLBACK_BASE_URL` 应该指向这个实例自己的容器名,例如:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://app-instance-alice:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## 15. 只看 auth-portal 页面
|
||||||
|
|
||||||
如果只想看 Portal 页面,不跑全链路:
|
如果只想看 Portal 页面,不跑全链路:
|
||||||
|
|
||||||
@ -608,7 +689,7 @@ http://127.0.0.1:3081
|
|||||||
|
|
||||||
注意:这只能看页面。注册、登录、创建实例仍依赖 `authz-service` 和 `deploy-control`。
|
注意:这只能看页面。注册、登录、创建实例仍依赖 `authz-service` 和 `deploy-control`。
|
||||||
|
|
||||||
## 15. 常用排错命令
|
## 16. 常用排错命令
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
|
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
|
||||||
@ -617,10 +698,13 @@ docker logs --tail=100 beaver-authz-service
|
|||||||
docker logs --tail=100 beaver-deploy-control
|
docker logs --tail=100 beaver-deploy-control
|
||||||
docker logs --tail=100 beaver-auth-portal
|
docker logs --tail=100 beaver-auth-portal
|
||||||
docker logs --tail=100 beaver-router-proxy
|
docker logs --tail=100 beaver-router-proxy
|
||||||
|
docker logs --tail=100 external-connector
|
||||||
|
|
||||||
curl http://127.0.0.1:19090/healthz
|
curl http://127.0.0.1:19090/healthz
|
||||||
curl http://127.0.0.1:8090/healthz
|
curl http://127.0.0.1:8090/healthz
|
||||||
curl -I http://127.0.0.1:3081
|
curl -I http://127.0.0.1:3081
|
||||||
|
curl -sS -H "Authorization: Bearer $EXTERNAL_CONNECTOR_TOKEN" \
|
||||||
|
"http://127.0.0.1:${EXTERNAL_CONNECTOR_PORT}/connectors"
|
||||||
```
|
```
|
||||||
|
|
||||||
实例创建失败时再看:
|
实例创建失败时再看:
|
||||||
@ -639,11 +723,14 @@ docker inspect beaver-authz-service --format '{{range .Config.Env}}{{println .}}
|
|||||||
|
|
||||||
docker inspect beaver-auth-portal --format '{{range .Config.Env}}{{println .}}{{end}}' \
|
docker inspect beaver-auth-portal --format '{{range .Config.Env}}{{println .}}{{end}}' \
|
||||||
| egrep '^(AUTHZ_API_BASE_URL|DEPLOY_API_BASE_URL)='
|
| egrep '^(AUTHZ_API_BASE_URL|DEPLOY_API_BASE_URL)='
|
||||||
|
|
||||||
|
docker inspect beaver-deploy-control --format '{{range .Config.Env}}{{println .}}{{end}}' \
|
||||||
|
| egrep '^(DEFAULT_EXTERNAL_CONNECTOR_BASE_URL|DEFAULT_EXTERNAL_CONNECTOR_TOKEN|DEFAULT_BEAVER_BRIDGE_TOKEN|DEFAULT_INITIAL_SKILLS_DIR)='
|
||||||
```
|
```
|
||||||
|
|
||||||
它们都必须是完整 URL,不能是空字符串,也不能是裸 `host:port`。
|
它们都必须是完整 URL,不能是空字符串,也不能是裸 `host:port`。
|
||||||
|
|
||||||
## 16. 常见问题
|
## 17. 常见问题
|
||||||
|
|
||||||
### 注册页报 URL 缺少协议
|
### 注册页报 URL 缺少协议
|
||||||
|
|
||||||
@ -727,23 +814,60 @@ getent hosts alice.localhost
|
|||||||
- `8088`
|
- `8088`
|
||||||
- `8090`
|
- `8090`
|
||||||
- `19090`
|
- `19090`
|
||||||
|
- `8787`(如果启用了 `external-connector`)
|
||||||
|
|
||||||
检查:
|
检查:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ss -ltnp | grep -E '3081|8088|8090|19090'
|
ss -ltnp | grep -E '3081|8088|8090|19090|8787'
|
||||||
```
|
```
|
||||||
|
|
||||||
## 17. 重新部署基础容器
|
### 连接器 sidecar 返回 401
|
||||||
|
|
||||||
只重建基础四个容器:
|
检查 `docker-compose.external-connectors.yml` 里 sidecar 使用的是 `CONNECTOR_API_TOKEN`,主部署变量名是 `EXTERNAL_CONNECTOR_TOKEN`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker inspect external-connector --format '{{range .Config.Env}}{{println .}}{{end}}' \
|
||||||
|
| egrep '^(CONNECTOR_API_TOKEN|BEAVER_BRIDGE_TOKEN|CONNECTOR_PROVIDER)='
|
||||||
|
```
|
||||||
|
|
||||||
|
请求 sidecar 管理 API 时必须使用:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer $EXTERNAL_CONNECTOR_TOKEN" \
|
||||||
|
"http://127.0.0.1:${EXTERNAL_CONNECTOR_PORT}/connectors"
|
||||||
|
```
|
||||||
|
|
||||||
|
如果改过 token,需要重启 `external-connector`、`beaver-deploy-control`,并重新创建或重建目标 app-instance。
|
||||||
|
|
||||||
|
### 微信或飞书连接成功但消息回不到实例
|
||||||
|
|
||||||
|
优先检查目标 app-instance 的回调地址:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker inspect "$INSTANCE_CONTAINER" --format '{{range .Config.Env}}{{println .}}{{end}}' \
|
||||||
|
| grep '^EXTERNAL_CONNECTOR_CALLBACK_BASE_URL='
|
||||||
|
```
|
||||||
|
|
||||||
|
多实例部署里它必须指向当前实例自己的容器名,例如:
|
||||||
|
|
||||||
|
```text
|
||||||
|
EXTERNAL_CONNECTOR_CALLBACK_BASE_URL=http://app-instance-alice:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
如果它为空,通常是实例创建时没有传 `--network "$BEAVER_NET"`,或者旧实例是在连接器变量加入前创建的。重新创建实例,或用同样的实例数据目录手工重建容器。
|
||||||
|
|
||||||
|
## 18. 重新部署基础容器
|
||||||
|
|
||||||
|
只重建基础容器和可选 sidecar:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker rm -f \
|
docker rm -f \
|
||||||
beaver-auth-portal \
|
beaver-auth-portal \
|
||||||
beaver-authz-service \
|
beaver-authz-service \
|
||||||
beaver-deploy-control \
|
beaver-deploy-control \
|
||||||
beaver-router-proxy 2>/dev/null || true
|
beaver-router-proxy \
|
||||||
|
external-connector 2>/dev/null || true
|
||||||
```
|
```
|
||||||
|
|
||||||
这不会自动删除实例数据。如果你还需要旧账号、旧实例或模型配置,不要删除 `runtime/` 目录。
|
这不会自动删除实例数据。如果你还需要旧账号、旧实例或模型配置,不要删除 `runtime/` 目录。
|
||||||
|
|||||||
Reference in New Issue
Block a user