Files
group_xinghuo_jinrong/app/api/auth_adapter.py
T
zhanghongyu_0626 b841f68295 feat(visitor): Implement visitor chat functionality and enhance customer service interactions
- Added a new visitor chat API endpoint (`/api/chat/visitor`) to allow unauthenticated users to engage in conversations without requiring customer data.
- Introduced a visitor context dependency to manage visitor interactions seamlessly.
- Enhanced the chat API to support explicit session termination and improved response handling for customer service interactions.
- Updated the database configuration to include Redis client support for caching visitor data.
- Added a new customer note repository to persist user notes independently of the L1 profile slots.

This update significantly improves the customer service experience by enabling visitor interactions and ensuring efficient data handling for both registered and unregistered users.
2026-09-09 18:32:00 +08:00

142 lines
5.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""宿主 AuthContext → 模块 AuthContext 适配器(S2 接缝,AL-09 接线用)。
状态:**AL-09 已接线(JWT 统一 + 适配器可用)**。
- `/api/auth/login` 与模块 API 共用 `settings.jwt_issuer` / `jwt_audience` 签发的 token;
- chat / risk 一律 `deps.get_auth_context` 验签;
- 若某宿主路由仍产出 `schemas.AuthContext`,经 `module_auth_from_host()` 转换后进入模块 Service。
设计取舍:
- **不 import 宿主模块**(main 的 `app/model/schemas.py` 等),改用鸭子类型读取字段,
保证本文件在当前分支(宿主文件尚不存在)也能被导入与单测;
- 只做字段映射与语义补齐,不做任何权限判定(判定仍在 `deps.py` 的矩阵内);
- 权限原样透传,另提供 `perm_matches` 兼容宿主的 `前缀:*` 通配语义,
避免模块侧 `has_permission` 精确匹配漏判通配权限。
字段映射(宿主 → 模块):
sub → actor_id (字段名差异,模块全量代码依赖 actor_id)
trace_id → contextvar 绑定 (模块不存字段,走 utils.trace.current_trace)
agent_type → 不入模,仅返回 (模块按 agent_type 单独传参做准入判定)
roles/permissions/tenant_id/jti/customer_id → 原样透传
"""
from __future__ import annotations
from typing import Any, NamedTuple
from app.api.deps import AuthContext
from app.utils.trace import set_trace
__all__ = ["from_host_auth", "perm_matches", "resolve_agent_type", "module_auth_from_host", "host_auth_for_customer_service", "HostAuthAdapterError"]
class _HostAuthShim(NamedTuple):
"""与宿主 schemas.AuthContext 同形(S2 反向接缝;不 import 宿主模块)。"""
sub: str
token_type: str
roles: list[str]
permissions: list[str]
tenant_id: str
trace_id: str
agent_type: str
jti: str
customer_id: str | None
class HostAuthAdapterError(ValueError):
"""宿主上下文字段缺失且无法安全兜底时抛出(fail-closed,不静默降级)。"""
def perm_matches(perm: str, permissions: list[str] | tuple[str, ...]) -> bool:
"""权限匹配:精确命中,或命中宿主的 `前缀:*` 通配写法。
例:`risk:alert:read` 在权限集含 `risk:*` 时返回 True。
"""
if not perm:
return False
if perm in permissions:
return True
prefix = perm.split(":", 1)[0]
return any(p == f"{prefix}:*" or p.endswith(":*") and p.startswith(f"{prefix}:") for p in permissions)
def from_host_auth(
host_ctx: Any,
*,
agent_type: str | None = None,
bind_trace: bool = True,
) -> AuthContext:
"""把宿主 AuthContext 转成模块 AuthContext。
Args:
host_ctx: 宿主的 `AuthContext`(含 sub/roles/token_type/... 的任意对象)。
agent_type: 显式指定请求目标 agent;缺省时取宿主的 `agent_type` 字段。
bind_trace: 是否把宿主的 `trace_id` 绑定到模块 trace contextvar。
Raises:
HostAuthAdapterError: 连主体标识(sub/actor_id)都取不到时抛出。
"""
actor_id = getattr(host_ctx, "sub", None) or getattr(host_ctx, "actor_id", None)
if not actor_id:
# fail-closed:拿不到主体就拒绝,不匿名放行
raise HostAuthAdapterError("宿主 AuthContext 缺少主体标识(sub/actor_id),拒绝放行")
roles = list(getattr(host_ctx, "roles", None) or [])
permissions = list(getattr(host_ctx, "permissions", None) or [])
token_type = getattr(host_ctx, "token_type", None) or "staff"
customer_id = getattr(host_ctx, "customer_id", None)
# customer 类 token 若未显式带 customer_id,回退为主体自身
if customer_id is None and token_type == "customer":
customer_id = actor_id
if bind_trace:
trace_id = getattr(host_ctx, "trace_id", None)
if trace_id:
set_trace(str(trace_id))
return AuthContext(
actor_id=str(actor_id),
roles=roles,
customer_id=customer_id,
token_type=str(token_type),
permissions=permissions,
tenant_id=getattr(host_ctx, "tenant_id", None),
jti=getattr(host_ctx, "jti", None),
)
def resolve_agent_type(host_ctx: Any, default: str = "risk") -> str:
"""取请求目标 agent 类型;宿主无该字段时回退 default。"""
value = getattr(host_ctx, "agent_type", None)
return str(value) if value else default
def module_auth_from_host(host_ctx: Any, *, agent_type: str | None = None) -> AuthContext:
"""S2 接缝接线入口:宿主 AuthContext → 模块 AuthContext(含 trace 绑定)。"""
resolved = agent_type or resolve_agent_type(host_ctx)
return from_host_auth(host_ctx, agent_type=resolved, bind_trace=True)
def host_auth_for_customer_service(module_auth: AuthContext, *, trace_id: str) -> _HostAuthShim:
"""模块 AuthContext → 宿主 AuthContext 同形对象(客服 customer_service 专用)。
customer_service 仍使用 schemas.AuthContext 字段名(sub/trace_id/agent_type),
chat 模块线走 deps.AuthContext;本函数为 S2 反向接缝,仅 customer 分流调用。
"""
customer_id = module_auth.customer_id
if customer_id is None and module_auth.token_type == "customer":
customer_id = module_auth.actor_id
return _HostAuthShim(
sub=module_auth.actor_id,
token_type=module_auth.token_type,
roles=list(module_auth.roles),
permissions=list(module_auth.permissions),
tenant_id=module_auth.tenant_id or "default",
trace_id=trace_id,
agent_type="customer",
jti=module_auth.jti or "",
customer_id=customer_id,
)