diff --git a/2.Design/screens/prod-integrations.png b/2.Design/screens/prod-integrations.png new file mode 100644 index 0000000..a4b322b Binary files /dev/null and b/2.Design/screens/prod-integrations.png differ diff --git a/backend/app/api/integrations.py b/backend/app/api/integrations.py index a6d2097..fdea5eb 100644 --- a/backend/app/api/integrations.py +++ b/backend/app/api/integrations.py @@ -45,6 +45,7 @@ from fastapi.responses import PlainTextResponse from sqlalchemy import select from sqlalchemy.exc import IntegrityError +from ..config import get_settings from ..db import bind_tenant from ..deps import AdminDep, SessionDep from ..errors import AppError, ErrorCode @@ -417,8 +418,14 @@ def _channel_out(channel: BotChannel) -> dict[str, Any]: def _webhook_url(request: Request, token: str) -> str: - base = str(request.base_url).rstrip("/") - return f"{base}/integrations/clawbot/{token}" + """拼出机器人要打的那个地址。 + + 不能直接用 `request.base_url`:生产上应用挂在域名根、路由带 `/api/v1` 前缀, + base_url 里没有前缀,拼出来的地址会 405(本地被 Vite 代理掩盖过这个差异)。 + 所以站点根从 base_url 里截,前缀一律取 `settings.api_prefix`。 + """ + base = str(request.base_url).split("/api/")[0].rstrip("/") + return f"{base}{get_settings().api_prefix}/integrations/clawbot/{token}" @admin_router.get("/{ws_id}/integrations") diff --git a/backend/tests/test_clawbot.py b/backend/tests/test_clawbot.py index c410369..ad0c94a 100644 --- a/backend/tests/test_clawbot.py +++ b/backend/tests/test_clawbot.py @@ -197,3 +197,11 @@ async def test_member_cannot_manage_channels(client: AsyncClient) -> None: ) assert resp.status_code == 403 assert resp.json()["code"] in (3004, 3003) + + +async def test_webhook_url_carries_api_prefix(client: AsyncClient) -> None: + """回调地址必须带 /api/v1:生产上应用挂在域名根,漏了前缀就是 405。""" + user = await login(client) + ws = await create_workspace(client, user, "微信地址") + channel = await _channel(client, user, ws) + assert channel["webhook_url"].endswith(f"/api/v1/integrations/clawbot/{channel['token']}")