Merge origin/qyqy_develop:风控时间口径补齐 + Agent 截断提示

组员的 4 个提交(相对 c11e56c):

- 4b0c148 把我这边的风控 P3 收尾与适当性豁免额度合并进 qyqy_develop(PR #4)
- 2ebe438 docs: 增加风控 Agent 工具白名单与意图配置
- 5102eaa Merge origin/qyqy_develop into RM2_develop
- d2cdbba 修复风控时间口径与 Agent 截断提示

自动合并零冲突,两侧的改动是互补而非重叠:

- 时区:我在 list_alerts 做了 from_local,他补的是 list_evidence 与通知列表;
- 截断:我加了 data_truncated / evidence_truncated,他在 risk_agent 的 system prompt
  里加了第 10 条 —— 这些标记出现时不得按全量证据下结论。方向是一致的。

验证:ruff 干净 / mypy 138 文件 / 703 unit+contract(+6,组员新增的用例)/
33 integration,均在合并后的工作树上跑过。
This commit is contained in:
2026-09-11 16:33:55 +08:00
26 changed files with 524 additions and 27 deletions
@@ -439,3 +439,10 @@ async def test_general_list_question_uses_complete_search_summary() -> None:
assert "CUST-002" in text
assert "稳健一号" in text
assert "全部命中记录" in text
def test_agent_prompt_requires_truncation_disclosure() -> None:
prompt = public_risk_agent._agent_system_prompt("查看当前预警")
assert "data_truncated=true" in prompt
assert "证据不完整" in prompt
@@ -1,5 +1,7 @@
from datetime import datetime
import pytest
from app.model.fund import FundRiskAlert
from app.service.risk_notification_service import RiskNotificationService
@@ -77,3 +79,47 @@ def test_high_risk_batch_creates_in_app_and_mail_records() -> None:
assert len(records) == 2
assert {record.channel for record in records} == {"站内提醒", "邮件"}
@pytest.mark.asyncio
async def test_notification_list_interprets_time_as_local_time(monkeypatch) -> None:
from app.api.schemas.risk import RiskNotificationPageQuery
from app.core.contracts import RequestContext
from app.repository.fund_query_repository import FundPage
captured: dict = {}
class StubRepository:
def __init__(self, _session, *, scope) -> None:
self.scope = scope
async def list_notifications(self, **kwargs) -> FundPage:
captured.update(kwargs)
return FundPage(
entity="risk_notification",
items=(),
limit=10,
offset=0,
next_offset=None,
)
monkeypatch.setattr(
"app.service.risk_notification_service.RiskRepository",
StubRepository,
)
service = RiskNotificationService(object())
query = RiskNotificationPageQuery(
start_time=datetime(2026, 9, 10, 12, 0),
end_time=datetime(2026, 9, 10, 13, 0),
)
context = RequestContext(
user_id="990000002",
trace_id="trace",
permissions=("risk:alert:read",),
data_scope="all",
)
await service.list_notifications(context, query)
assert captured["start_time"] == datetime(2026, 9, 10, 4, 0)
assert captured["end_time"] == datetime(2026, 9, 10, 5, 0)
@@ -52,6 +52,28 @@ class FakeRepository:
return None
class RecordingRepository(FakeRepository):
def __init__(self) -> None:
super().__init__()
self.calls: list[tuple[str, dict]] = []
async def list_transactions(self, **kwargs) -> FundPage:
self.calls.append(("transactions", kwargs))
return self.page
async def list_capital_flows(self, **kwargs) -> FundPage:
self.calls.append(("capital_flows", kwargs))
return self.page
async def list_login_records(self, **kwargs) -> FundPage:
self.calls.append(("login_records", kwargs))
return self.page
async def list_notifications(self, **kwargs) -> FundPage:
self.calls.append(("notifications", kwargs))
return self.page
def context(**updates) -> RequestContext:
values = {
"user_id": "990000002",
@@ -163,3 +185,23 @@ async def test_missing_alert_detail_is_hidden() -> None:
with pytest.raises(GenericResourceNotFoundError):
await service.get_alert_detail(context(), "ALERT-NOT-FOUND")
@pytest.mark.asyncio
@pytest.mark.parametrize(
"source",
("transactions", "capital_flows", "login_records", "notifications"),
)
async def test_evidence_time_filters_are_interpreted_as_local_time(source: str) -> None:
repository = RecordingRepository()
service = RiskQueryService(None, repository=repository)
query = RiskEvidencePageQuery(
start_time=datetime(2026, 9, 10, 12, 0),
end_time=datetime(2026, 9, 10, 13, 0),
)
await service.list_evidence(context(), source, query)
assert repository.calls[0][0] == source
assert repository.calls[0][1]["start_time"] == datetime(2026, 9, 10, 4, 0)
assert repository.calls[0][1]["end_time"] == datetime(2026, 9, 10, 5, 0)