Files
group_fqcd_jr/app/core/actor.py
T
张胜宇 9675df8453 chore(sync): zsy_developcc 全量同步至 qyqy_develop(W26 口径)
- 分支内容对齐 qyqy_develop b6ec3aa,树完全一致(同步后 git diff 为空)
- 覆盖本轮全部交付:客服 Agent 重构(安全路由 / 五出口 / 记忆与画像 / RAG 全链路)
  + 开发文档 62 份编号体系(D1.1 v1.17 索引)
  + 新增 D2.10-客服Agent端到端答辩文档-2026-09-21.html
- 基线:e239eb7(2026-09-17 品牌口径统一快照),本提交为其直接后继
2026-09-21 21:26:30 +08:00

60 lines
2.8 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.
"""访客(匿名)主体的**唯一构造点**与统一判定谓词。
为什么需要这个模块(`G-01` / `G-01b`):
1. **构造点有两份**。访客三元组(角色 / 权限 / 数据范围)此前在
``app/core/security.py``(API 入口)与 ``app/worker/runtime.py``(Worker 执行路径)
各手写了一份。两份只要漂移一次,就会出现「API 认得访客、Worker 不认得」这类
只在异步链路才暴露的越权面,而且**没有测试能发现**——因为它不是同一个函数。
2. **判定散落各处**。``"visitor" in context.roles`` 这个判定此前出现在 6 个文件、
13 处。口径要改(例如以后加身份轴)就得改 13 个地方,漏一处就是一个缺口。
因此:构造只走 :func:`anonymous_context`,判定只走 :func:`is_visitor`。
两者的行为都与改造前**逐字一致**——本模块是重构,不是行为变更。
"""
from __future__ import annotations
from app.core.contracts import RequestContext
#: 访客角色名。与 `app/core/knowledge_tier.TIERS_BY_SUBJECT` 的键同名同源(`G-03` 落点)。
VISITOR_ROLE = "visitor"
#: 受理事件里持久化的 actor 类型取值(`conversation_session` / 审计侧)。
VISITOR_ACTOR_TYPE = "visitor"
#: 访客角色元组。**唯一副本**:`security.py` 与 `runtime.py` 都从这里取。
VISITOR_ROLES: tuple[str, ...] = (VISITOR_ROLE,)
#: 访客仅可运行 Agent 与读取已发布的公共知识,**不含任何个人数据权限**。
#: 不允许在这里追加权限码——访客能力扩张属权限决策,须单独评审。
VISITOR_PERMISSIONS: tuple[str, ...] = ("agent:run", "knowledge:query")
#: 访客数据范围恒为 `public`(与知识侧档位 `{public}` 同一收敛方向)。
VISITOR_DATA_SCOPE = "public"
def anonymous_context(*, user_id: str, trace_id: str, portal: str = "api") -> RequestContext:
"""构造访客请求上下文。**访客上下文的唯一来源。**
`user_id` 是令牌 `sub`(数字串,由 `VisitorTokenIssuer` 生成),不是正式用户主键;
`portal` 默认 `api`,Worker 侧恢复上下文时同样用默认值,保证两侧逐字相等。
"""
return RequestContext(
user_id=str(user_id),
trace_id=str(trace_id),
roles=VISITOR_ROLES,
permissions=VISITOR_PERMISSIONS,
data_scope=VISITOR_DATA_SCOPE,
portal=portal,
)
def is_visitor(context: RequestContext) -> bool:
"""访客判定。**全仓唯一谓词**——不得再写 ``"visitor" in context.roles``。
保留在基类 / 依赖注入层调用(`G-01b` 的底线:判定位置与层级不变,
只是换成一个统一函数),因此这里**不读 Agent 声明位**、也不依赖任何配置。
"""
return VISITOR_ROLE in context.roles