fix(agent): route agent command receipts to the signed channel user (#7228)

- Agent 代渠道用户调用内部 API 时,把会话渠道与渠道用户写入宿主签发令牌的签名
  声明(agent_channel、agent_channel_user),不再新增可伪造的请求头。
- /commands/run 对 Telegram 等通知渠道只使用与请求渠道一致的签名渠道用户执行命令
  和投递回执;缺少该声明时明确拒绝,不再把 MoviePilot 用户表主键当作渠道 chat id。
  Web、WebAgent 与无渠道调用仍按 MoviePilot 用户 ID 处理。
- 只读公共资料借用宿主身份,不携带渠道发起人声明。
- 同步依赖基线与架构总览中的内部导入边数量。

Refs #7225

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: jxxghp <jxxghp@gmail.com>
This commit is contained in:
Cat
2026-10-28 19:05:52 +08:00
committed by GitHub
parent 252b702875
commit 87561a90d1
17 changed files with 485 additions and 32 deletions

View File

@@ -345,3 +345,47 @@ def test_executor_keeps_ascii_session_header_verbatim() -> None:
headers = _build_context_headers(session_id)
assert headers["X-MoviePilot-Agent-Session"] == session_id
@pytest.mark.parametrize("channel,channel_user_id", [
(NotificationChannel.Telegram.value, "123456789"),
(NotificationChannel.Wechat.value, "张三"),
(NotificationChannel.Telegram.value, None),
(None, "123456789"),
])
def test_executor_signs_channel_user_into_host_token(channel, channel_user_id) -> None:
"""
渠道发起人只写入宿主令牌的签名声明,不再以可伪造的请求头透传。
"""
executor = MoviePilotApiExecutor(
context=ApiExecutionContext(
user_id="1",
username="admin",
is_admin=True,
channel=channel,
channel_user_id=channel_user_id,
),
request_factory=MagicMock(return_value=SimpleNamespace(request=AsyncMock())),
)
with patch("app.agent.api.executor.create_access_token", return_value="token") as create_token:
headers = executor._build_headers()
signed = bool(channel and channel_user_id)
assert create_token.call_args.kwargs.get("agent_channel") == (channel if signed else None)
assert create_token.call_args.kwargs.get("agent_channel_user") == (channel_user_id if signed else None)
assert not any(name.lower() == "x-moviepilot-agent-user" for name in headers)
def test_public_executor_never_signs_channel_user() -> None:
"""
公共只读执行器即使收到渠道上下文,也不能把个人发起人绑定到宿主身份。
"""
executor = MoviePilotApiExecutor(context=ApiExecutionContext(
user_id="1", username="admin", is_admin=True, public_read_only=True,
channel=NotificationChannel.Telegram.value, channel_user_id="123456789",
))
with patch("app.agent.api.executor.create_access_token", return_value="token") as create_token:
executor._build_headers()
create_token.assert_called_once_with(userid=1, username="admin", super_user=True)