修复风控时间口径与Agent截断提示
This commit is contained in:
@@ -362,7 +362,15 @@ def _agent_system_prompt(message: str) -> str:
|
||||
"8. 查询结果包含 summary 时,客户、产品和规则数量必须依据完整 summary,"
|
||||
"不能因为 items 被截断就回答只覆盖部分记录。\n"
|
||||
"9. 最终回答使用中文,简洁说明结论、依据和剩余风险,并提醒由风控专员人工复核。\n"
|
||||
f"{context}\n{filter_context}"
|
||||
f"{context}\n{filter_context}\n{_truncation_instruction()}"
|
||||
)
|
||||
|
||||
|
||||
def _truncation_instruction() -> str:
|
||||
return (
|
||||
"10. 工具结果出现 data_truncated=true、evidence_truncated 非空或 "
|
||||
"truncated=true 时,必须明确说明当前证据不完整,不能按全量证据下结论;"
|
||||
"只有结果明确完整时,才能表述为覆盖全部记录。"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.api.schemas.risk import RiskNotificationPageQuery
|
||||
from app.core.contracts import RequestContext
|
||||
from app.core.risk_cursor import decode_offset_cursor
|
||||
from app.core.timeutil import from_local
|
||||
from app.model.fund import FundRiskAlert, FundRiskNotification
|
||||
from app.repository.fund_query_repository import PageRequest
|
||||
from app.repository.risk_repository import RiskRepository
|
||||
@@ -38,8 +39,8 @@ class RiskNotificationService:
|
||||
).list_notifications(
|
||||
keyword=query.keyword,
|
||||
send_status=query.send_status,
|
||||
start_time=query.start_time,
|
||||
end_time=query.end_time,
|
||||
start_time=from_local(query.start_time) if query.start_time else None,
|
||||
end_time=from_local(query.end_time) if query.end_time else None,
|
||||
page=PageRequest(
|
||||
limit=query.limit,
|
||||
offset=decode_offset_cursor(query.cursor, binding=binding),
|
||||
|
||||
@@ -112,6 +112,8 @@ class RiskQueryService:
|
||||
offset=decode_offset_cursor(query.cursor, binding=binding),
|
||||
)
|
||||
repository = self._repository(context)
|
||||
start_time = from_local(query.start_time) if query.start_time else None
|
||||
end_time = from_local(query.end_time) if query.end_time else None
|
||||
if source == "customers":
|
||||
page = await repository.list_customers(
|
||||
keyword=query.keyword,
|
||||
@@ -123,15 +125,15 @@ class RiskQueryService:
|
||||
elif source == "transactions":
|
||||
page = await repository.list_transactions(
|
||||
keyword=query.keyword,
|
||||
start_time=query.start_time,
|
||||
end_time=query.end_time,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
page=page_request,
|
||||
)
|
||||
elif source == "capital_flows":
|
||||
page = await repository.list_capital_flows(
|
||||
keyword=query.keyword,
|
||||
start_time=query.start_time,
|
||||
end_time=query.end_time,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
page=page_request,
|
||||
)
|
||||
elif source == "holdings":
|
||||
@@ -139,8 +141,8 @@ class RiskQueryService:
|
||||
elif source == "login_records":
|
||||
page = await repository.list_login_records(
|
||||
keyword=query.keyword,
|
||||
start_time=query.start_time,
|
||||
end_time=query.end_time,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
page=page_request,
|
||||
)
|
||||
elif source == "alerts":
|
||||
@@ -150,8 +152,8 @@ class RiskQueryService:
|
||||
page = await repository.list_notifications(
|
||||
keyword=query.keyword,
|
||||
send_status=query.send_status,
|
||||
start_time=query.start_time,
|
||||
end_time=query.end_time,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
page=page_request,
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
| 允许角色 | `risk_operator`、`admin` |
|
||||
| 允许入口 | `api` |
|
||||
| 只读工具 | 风险概览、预警查询、预警证据 |
|
||||
| 权限 | `agent:run`、`risk:alert:read`、`risk:alert:write`、`risk:alert:scan` |
|
||||
| 权限 | `agent:run`、`risk:alert:read`、`risk:alert:write`、`risk:alert:scan`、`risk:report:mail` |
|
||||
|
||||
## 数据库依赖
|
||||
|
||||
@@ -48,8 +48,8 @@
|
||||
|
||||
- 主项目路由能够访问本模块只读查询接口。
|
||||
- 风控账号拥有角色和对应权限。
|
||||
- 风控写接口和扫描接口支持并携带 `Idempotency-Key`。
|
||||
- 风控账号具有有效的客户归属数据。
|
||||
- Agent Run 能够调用本模块工具。
|
||||
- 工具调用、处置和权限拒绝能够写入审计。
|
||||
- Redis 或 Milvus 不可用时,结构化风控功能仍可用。
|
||||
|
||||
|
||||
@@ -8,11 +8,25 @@
|
||||
|
||||
- 路由前缀为 `/api/v1/risk`。
|
||||
- 响应使用主项目 `{data, meta}` 信封。
|
||||
- 列表接口的 `data` 为数组,`next_cursor` 和 `has_more` 放在 `meta`。
|
||||
- 时间和日期使用 RFC 3339 或主项目约定格式。
|
||||
- 金额、数量和主键按主项目字段映射返回字符串。
|
||||
- 预警队列每页最多 5 条,其他证据列表每页最多 10 条。
|
||||
- 未授权请求返回主项目统一权限错误。
|
||||
|
||||
列表响应格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [],
|
||||
"meta": {
|
||||
"trace_id": "trace-id",
|
||||
"next_cursor": "opaque-cursor",
|
||||
"has_more": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 只读接口
|
||||
|
||||
| 方法 | 路径 | 功能 | 权限 |
|
||||
@@ -122,3 +136,9 @@
|
||||
| `customer_id` | 兼容字段,值与 `customer_no` 相同,仅供既有前端继续使用 |
|
||||
|
||||
内部 `fin_customer_profile.customer_id` 和 `sys_user.id` 不向预警详情接口暴露。
|
||||
|
||||
## 时间参数
|
||||
|
||||
- 带时区的时间按自身时区解释。
|
||||
- 不带时区的 REST 时间参数按 `Asia/Shanghai` 解释,再转换为 UTC 查询。
|
||||
- 客户端不能继续假设裸时间是 UTC。
|
||||
|
||||
@@ -36,6 +36,7 @@ JWT 身份
|
||||
| `risk:alert:read` | 概览、预警、详情、证据、通知、日报、Agent 只读工具 |
|
||||
| `risk:alert:write` | 确认、调查、误报、结案、升级、证据归档 |
|
||||
| `risk:alert:scan` | 手动或受控规则扫描 |
|
||||
| `risk:report:mail` | 发送日报邮件 |
|
||||
|
||||
## 客户数据范围
|
||||
|
||||
@@ -55,6 +56,17 @@ WHERE employee_id = 当前用户ID
|
||||
- 只能访问有效分配客户的预警和相关证据。
|
||||
- 权限决定功能访问,客户归属决定数据访问。
|
||||
- 权限拒绝必须追加审计。
|
||||
- 写接口和扫描接口必须同时携带有效 `Idempotency-Key`。
|
||||
|
||||
## 当前数据范围风险
|
||||
|
||||
公共身份层当前取用户所有权限中的最高 `data_scope`:
|
||||
|
||||
```text
|
||||
任意权限为 all -> context.data_scope=all
|
||||
```
|
||||
|
||||
这可能导致一个权限的 `all` 范围扩散到其他资源。主项目合并后应改为按权限码分别应用数据范围,避免风控客户范围被其他资源的全量权限绕过。
|
||||
|
||||
## 管理角色
|
||||
|
||||
@@ -69,4 +81,3 @@ WHERE employee_id = 当前用户ID
|
||||
- 未分配客户不能通过直接请求越权访问。
|
||||
- 缺少写入权限时不能执行处置。
|
||||
- 权限或客户归属失效后,下一次请求立即生效。
|
||||
|
||||
|
||||
@@ -74,3 +74,9 @@
|
||||
- 证据归档先写文件,成功后更新证据快照;失败不得留下业务状态脏数据。
|
||||
- 审计只允许追加,不允许普通业务接口修改或删除。
|
||||
|
||||
## 查询上限与索引
|
||||
|
||||
- `fin_risk_alert.trigger_rule_codes` 增加 JSON 多值索引,用于规则编号筛选。
|
||||
- 预警详情中的资金流、持仓和登录记录分别最多返回 200 条。
|
||||
- 日报每组预警最多读取 5000 条。
|
||||
- 超限时通过 `evidence_truncated` 或 `data_truncated` 显式标记,不能伪装为全量结果。
|
||||
|
||||
@@ -49,6 +49,16 @@
|
||||
- 累计最多 6 次工具调用。
|
||||
- 单次工具结果超过限制时截断,但完整分组汇总优先保留。
|
||||
|
||||
模型端点按任务能力筛选:
|
||||
|
||||
- `risk_agent_chat`
|
||||
- `risk_analysis`
|
||||
- `risk_script`
|
||||
- `risk_summary`
|
||||
- `daily_report_suggestion`
|
||||
|
||||
以上任务均要求文本生成能力,不能落回 embedding 端点。未登记的任务类型会告警并退回全部 active 端点。
|
||||
|
||||
## 完整回答原则
|
||||
|
||||
- 查询结果包含 `summary` 时,客户、产品和规则数量以完整汇总为准。
|
||||
@@ -81,4 +91,3 @@
|
||||
- 成功、失败或拒绝状态。
|
||||
- 拒绝原因或执行结果摘要。
|
||||
- `trace_id`。
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
|
||||
当前业务状态发生变化时,应重新读取当前证据复核,不能只依赖历史摘要。
|
||||
|
||||
资金流、持仓和登录记录单次最多返回 200 条。`evidence_truncated` 非空时,表示对应证据被截断,不能当作完整证据链。
|
||||
|
||||
## 脱敏要求
|
||||
|
||||
- 客户姓名只保留首字,其余用 `*`。
|
||||
@@ -52,6 +54,7 @@
|
||||
- 日志、模型输入和审计摘要不得记录明文密码或密钥。
|
||||
- 敏感编号只保留业务所需最小信息。
|
||||
- 文件归档不得覆盖其他预警证据。
|
||||
- 归档审计必须写入 `created_at`,否则数据库非空约束会让上传返回 500。
|
||||
|
||||
## 文件归档
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
历史未闭环不受分页限制。
|
||||
|
||||
- 日界按 `Asia/Shanghai` 计算,再转换为 UTC 查询。
|
||||
- 每组预警最多读取 5000 条;超限时返回 `data_truncated=true`。
|
||||
|
||||
## 九段式模板
|
||||
|
||||
1. 当日预警数量。
|
||||
@@ -40,12 +43,15 @@
|
||||
- 高风险预警可以生成站内或邮件通知记录。
|
||||
- 通知内容包含预警编号,便于定位原始预警。
|
||||
- 通知发送失败不能回滚已经生成的预警。
|
||||
- 扫描结果通过 `notification_failure` 暴露通知创建失败原因,不能再把失败与无需通知都显示成 `0`。
|
||||
- 通知查询按主项目分页和权限规则执行。
|
||||
- 数据库保留 `read_at`、`acknowledged_at` 基线字段,但当前未实现写入逻辑。
|
||||
- 通知接口和前端不返回或展示阅读时间、确认时间。
|
||||
|
||||
## 邮件开关
|
||||
|
||||
发送日报邮件需要 `risk:report:mail` 权限。
|
||||
|
||||
日报邮件默认不发送真实邮件。主要配置项:
|
||||
|
||||
- `RISK_DAILY_REPORT_MAIL_ENABLED`
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
| `risk_evidence_archived` | 证据归档 | 预警编号、文件信息 |
|
||||
| `risk_ai_analysis_generated` | 生成研判、话术或摘要 | 输出类型和来源 |
|
||||
| `risk_daily_report_generated` | 生成日报 | 报表统计摘要 |
|
||||
| `risk_scan_scheduled_succeeded` | 定时扫描成功 | 扫描计数、尝试次数 |
|
||||
| `risk_scan_scheduled_failed` | 定时扫描失败 | 尝试次数、错误类型 |
|
||||
|
||||
## Agent 和权限审计
|
||||
|
||||
@@ -42,6 +44,13 @@
|
||||
| `agent.access_denied` | 创建运行时权限拒绝 |
|
||||
| `permission.denied` | Service 权限校验失败 |
|
||||
|
||||
风控写接口还会写入 `api_request_receipt`:
|
||||
|
||||
- 绑定用户、路径、幂等键和请求摘要。
|
||||
- 重复请求返回首次结果。
|
||||
- 同键不同请求返回 `409 IDEMPOTENCY_CONFLICT`。
|
||||
- 证据上传目前依靠“同一预警只能归档一次”实现业务防重。
|
||||
|
||||
## 追踪建议
|
||||
|
||||
排查一次 Agent 对话时,可以按以下顺序关联:
|
||||
@@ -69,4 +78,3 @@ alert_no
|
||||
- 敏感原文不直接写入详情。
|
||||
- 工具参数只保留必要摘要。
|
||||
- 权限拒绝也属于审计事件。
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
3. 风控账号访问未分配客户,应失败关闭。
|
||||
4. 缺少写权限时,人工处置接口应拒绝。
|
||||
5. 权限拒绝应写入审计。
|
||||
6. `risk_operator` 应具备 `risk:alert:read`、`risk:alert:write`、`risk:alert:scan` 和 `risk:report:mail`。
|
||||
7. 风控写接口和扫描接口应携带有效 `Idempotency-Key`。
|
||||
|
||||
## 业务验收
|
||||
|
||||
@@ -37,6 +39,7 @@
|
||||
- 定时扫描配置开启后按周期执行。
|
||||
- 定时扫描默认关闭,多个 Worker 同时运行时不重复执行。
|
||||
- 定时扫描成功和失败写入系统审计。
|
||||
- 扫描返回 `notification_failure` 时,页面必须区分成功与通知失败。
|
||||
- 重复扫描不重复生成同一交易和规则的预警。
|
||||
- 多规则命中时正确合并。
|
||||
|
||||
@@ -51,7 +54,10 @@
|
||||
### 证据和归档
|
||||
|
||||
- 八类证据可以筛选和分页。
|
||||
- 列表接口使用 `data` 数组,分页字段位于 `meta.next_cursor`、`meta.has_more`。
|
||||
- 游标必须绑定当前用户、查询条件和证据路径。
|
||||
- 预警详情聚合交易、产品、工单、资金、持仓和登录证据。
|
||||
- `evidence_truncated` 非空时,页面和 Agent 必须提示证据被截断。
|
||||
- 合法文件归档成功。
|
||||
- 非法文件和重复归档被拒绝。
|
||||
|
||||
@@ -59,8 +65,10 @@
|
||||
|
||||
- 日报包含九段式内容。
|
||||
- 历史未闭环不受分页限制。
|
||||
- 日报日界按北京时间计算。
|
||||
- `data_truncated=true` 时不得把计数当作全量。
|
||||
- 误报、处置结果和规则效果正确统计。
|
||||
- 邮件开关关闭时不发送真实邮件。
|
||||
- 邮件接口要求 `risk:report:mail`,开关关闭时不发送真实邮件。
|
||||
|
||||
## Agent 验收
|
||||
|
||||
|
||||
@@ -51,7 +51,9 @@
|
||||
## 后续建议
|
||||
|
||||
- 主项目合并完成后对齐统一 RBAC 和客户数据范围。
|
||||
- 修复公共 `data_scope` 最高权限跨资源扩散问题。
|
||||
- 确认幂等回执与业务 Action 内部提交的事务边界。
|
||||
- 私有前端最后再适配 `data/meta` 列表信封和写接口 `Idempotency-Key`。
|
||||
- 根据合规要求确定会话保留期、脱敏和归档策略。
|
||||
- 正式前端接入前完成接口字段最终冻结。
|
||||
- 在网络可用时保留 Ruff、MyPy 和结构审计结果作为合并证据。
|
||||
|
||||
|
||||
@@ -86,6 +86,9 @@
|
||||
|
||||
- 所有风控 REST 请求使用 `/api/v1/risk`。
|
||||
- 使用主项目统一响应信封和错误处理。
|
||||
- 列表接口的 `data` 必须是数组,游标和 `has_more` 从 `meta` 读取。
|
||||
- 风控写接口和扫描接口必须生成并携带唯一 `Idempotency-Key`。
|
||||
- `Idempotency-Key` 只用于写请求,证据上传暂不要求。
|
||||
- Agent 对话使用 `/api/v1/agent-runs` 和 SSE 事件。
|
||||
- 不在前端实现另一套 Agent 对话协议。
|
||||
- SSE 需要处理 `start`、`tools`、`delta`、`replace`、`done` 和 `error`。
|
||||
@@ -157,6 +160,8 @@ error.message
|
||||
### 交互要求
|
||||
|
||||
- 操作进行中禁用重复点击,并显示进行中状态。
|
||||
- 写请求失败时不得复用同一个 `Idempotency-Key` 提交不同内容。
|
||||
- 写请求超时后可使用同一请求体和同一键重试,以获取首次结果。
|
||||
- 成功或失败必须使用主项目统一的消息、Toast 或通知组件。
|
||||
- 不能只用控制台日志代替用户提示。
|
||||
- 弹窗关闭前必须明确操作结果。
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
|
||||
| 项目 | 当前状态 |
|
||||
|---|---|
|
||||
| 统计日期 | 2026-09-10 |
|
||||
| 统计日期 | 2026-09-11 |
|
||||
| 当前分支 | `RM2_develop` |
|
||||
| 当前提交 | `a94d5c7` |
|
||||
| 提交信息 | `feat: 迁移奶龙风控业务模块与演示文档` |
|
||||
| 当前合并基线 | `origin/qyqy_develop` 主项目风控修复批次 |
|
||||
| 代码状态 | 已完成主项目风控修复合并,全量测试通过 |
|
||||
| 已推送分支 | `origin/RM2_develop` |
|
||||
| 已合并分支 | `origin/qyqy_develop` |
|
||||
| 私有前端 | `private_frontend/`,未提交、未推送 |
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
| 范围 | 完成度 | 说明 |
|
||||
|---|---:|---|
|
||||
| 后端业务模块 | 96% | 主要业务功能和定时规则扫描调度均已完成 |
|
||||
| 后端业务模块 | 98% | 主要业务功能和主项目风控修复均已合并,仍有少量后端加固项 |
|
||||
| 私有验证前端 | 90% | 可用于本地功能验证,但不作为公共正式前端 |
|
||||
| 主项目正式前端 | 10% | 尚未按主项目设计系统和正式页面结构合并 |
|
||||
| 主项目联调与验收 | 60% | 代码已合并,仍待主项目环境完整联调和正式前端接入 |
|
||||
@@ -93,7 +93,7 @@
|
||||
|
||||
| 检查项 | 结果 |
|
||||
|---|---|
|
||||
| 全量测试 | `567 passed, 1 skipped` |
|
||||
| 全量测试 | `697 passed, 1 skipped` |
|
||||
| Ruff 静态检查 | 通过 |
|
||||
| 风控专项测试 | 通过 |
|
||||
| 真实 Agent Run 验收 | 三类业务对话通过 |
|
||||
@@ -103,6 +103,15 @@
|
||||
| 依赖检查 | `pip check` 通过 |
|
||||
| 依赖声明 | 已补充 `python-dotenv`、`python-multipart`、`greenlet`、`tzdata` |
|
||||
|
||||
主项目新增并已合入:
|
||||
|
||||
- 列表信封、游标绑定和 JSON 多值索引。
|
||||
- 风控写接口幂等和 SSE 内容协商。
|
||||
- 邮件权限、RBAC 权限补齐脚本。
|
||||
- 通知失败可观测、扫描脏数据隔离和调度重启执行。
|
||||
- RW-007 豁免额度、RW-012、RW-015、RW-018 研判修复。
|
||||
- 日报北京时间、误报原因和截断标记。
|
||||
|
||||
## 未完成和暂缓事项
|
||||
|
||||
### 对话历史与长期留存
|
||||
@@ -152,9 +161,11 @@
|
||||
| 阻塞项 | 影响 | 处理方式 |
|
||||
|---|---|---|
|
||||
| 正式前端未合并 | 无法按主项目正式界面演示 | 按前端提示词文档执行合并 |
|
||||
| 私有前端尚未适配新信封和幂等请求头 | 预警列表、分页和写操作会失败 | 后端处理完成后统一改造 |
|
||||
| 主项目完整联调未完成 | 跨模块权限、导航和接口仍需验证 | 在 qyqy_develop 环境联调 |
|
||||
| 对话历史暂缓 | 跨轮长期记忆能力有限 | 迁移完成后单独实施 |
|
||||
| 公共底座 MyPy 例外 | 全量类型检查无法完全通过 | 保持例外或由主项目后续处理 |
|
||||
| 数据范围最高权限扩散 | 任意 `all` 权限可能绕过风控客户范围 | 改为按 permission_code 应用 scope |
|
||||
| 幂等回执与业务提交原子性 | 内部 commit 后回执提交前崩溃时无法回放结果 | 评估统一事务边界 |
|
||||
|
||||
## 下一阶段建议
|
||||
|
||||
|
||||
@@ -117,3 +117,12 @@ JSON 文件:
|
||||
- 确认 `risk` Agent 已注册到 `AgentFactory`。
|
||||
- 发起一次风险概览和一次预警搜索验证工具调用。
|
||||
|
||||
## 发布脚本
|
||||
|
||||
主项目已提供:
|
||||
|
||||
```text
|
||||
tools/publish_risk_agent_config.py
|
||||
```
|
||||
|
||||
该脚本用于发布工具白名单、意图配置和风险 Agent 所需权限。部署时应优先使用脚本,避免手工写入配置表造成工具白名单和意图配置不一致。
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# 风控模块合并后自身待办清单
|
||||
|
||||
## 文档定位
|
||||
|
||||
本文只记录风控模块自身在合并后需要处理的问题和联调动作。
|
||||
|
||||
公共底座、公共事务能力、公共数据范围、公共鉴权基座和公共 SSE 协商等问题不纳入本文,
|
||||
由主项目统一修复和发布。
|
||||
|
||||
## 本轮已完成
|
||||
|
||||
### 1. 证据查询时间口径统一
|
||||
|
||||
已完成:
|
||||
|
||||
- `/api/v1/risk/evidence/transactions`
|
||||
- `/api/v1/risk/evidence/capital_flows`
|
||||
- `/api/v1/risk/evidence/login_records`
|
||||
- `/api/v1/risk/evidence/notifications`
|
||||
|
||||
以上入口收到不带时区的开始时间和结束时间时,按北京时间解释,再转换为 UTC
|
||||
数据库查询时间,避免误查前后 8 小时的数据。
|
||||
|
||||
### 2. 通知记录查询时间口径统一
|
||||
|
||||
`/api/v1/risk/notifications` 已与本模块其他 REST 查询保持一致:
|
||||
|
||||
- 客户端裸时间按北京时间解释。
|
||||
- 查询前统一转换为 UTC。
|
||||
- 分页游标继续绑定用户、筛选条件和路径。
|
||||
|
||||
### 3. Agent 截断提示已补齐
|
||||
|
||||
奶龙风控智能助手在工具结果出现以下标记时,必须明确说明当前证据不完整:
|
||||
|
||||
- `data_truncated=true`
|
||||
- `evidence_truncated` 非空
|
||||
- `truncated=true`
|
||||
|
||||
Agent 不能把截断结果表述成覆盖全部数据,也不能据此给出确定性的全量结论。
|
||||
|
||||
### 4. 风控验收脚本密钥路径统一
|
||||
|
||||
以下脚本已改为读取 `JWT_PRIVATE_KEY_PATH`:
|
||||
|
||||
- `tools/risk_agent_e2e.py`
|
||||
- `tools/risk_agent_business_e2e.py`
|
||||
|
||||
不再硬编码 `config/jwt/jwt-private.pem`。
|
||||
|
||||
### 5. 权限初始化脚本说明修正
|
||||
|
||||
`tools/grant_risk_permissions.py` 的文档字符串已改为实际文件名,
|
||||
避免执行人员按错误路径操作。
|
||||
|
||||
## 联调前需要执行的动作
|
||||
|
||||
以下内容属于环境准备或数据初始化,不是代码缺陷:
|
||||
|
||||
1. 执行 `python tools/grant_risk_permissions.py`,确认风控角色具备:
|
||||
- `risk:alert:read`
|
||||
- `risk:alert:write`
|
||||
- `risk:alert:scan`
|
||||
- `risk:report:mail`
|
||||
2. 执行 `python tools/publish_risk_agent_config.py`,确认奶龙风控智能助手的工具白名单和意图配置已发布。
|
||||
3. 按主项目发布的迁移流程执行 Alembic 升级,确认 `trigger_rule_codes` 多值索引已生效。
|
||||
4. 演示前准备足够的客户、交易、资金、持仓、登录和预警数据。
|
||||
5. 使用前确认日报邮件开关、SMTP 配置和收件人范围符合演示要求。
|
||||
|
||||
## 当前保留限制
|
||||
|
||||
- 私有前端适配不纳入本文,等后端事项稳定后单独处理。
|
||||
- `read_at` 和 `acknowledged_at` 只保留数据库能力,通知接口不返回,前端不展示。
|
||||
- 定时扫描 Worker 启动后的首轮执行语义与 `RISK_SCAN_RUN_IMMEDIATELY` 的旧说明存在差异,
|
||||
当前代码优先保证“开关启用后不会永不扫描”。如后续要恢复严格的首次执行开关语义,
|
||||
需要单独设计执行时间的持久化和恢复方案。
|
||||
|
||||
## 验证要求
|
||||
|
||||
合并或联调前至少完成:
|
||||
|
||||
1. 风控专项测试通过。
|
||||
2. Ruff 检查通过。
|
||||
3. 全量测试无新增失败。
|
||||
4. Agent 实际对话、证据查询、通知查询和日报邮件分别完成一次人工联调。
|
||||
@@ -41,6 +41,7 @@
|
||||
| 18 | 当前项目完成进度 | 汇总当前完成度、验证结果和剩余任务 |
|
||||
| 19 | 风控模块配置项清单 | 单独说明风控专用和公共依赖配置 |
|
||||
| 20 | Agent工具白名单与意图配置 | 交付主项目方的工具和意图配置 |
|
||||
| 21 | 主项目合并后后端必改清单 | 列出合并后必须处理的后端事项 |
|
||||
|
||||
## 推荐阅读顺序
|
||||
|
||||
@@ -53,3 +54,4 @@
|
||||
7. 18:项目汇报、进度同步和下一阶段安排。
|
||||
8. 19:合并部署和联调时核对环境变量。
|
||||
9. 20:主项目方导入 Agent 工具白名单和意图配置。
|
||||
10. 21:主项目代码合并后的后端整改与验收。
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
幂等:权限与绑定都已存在时直接跳过。
|
||||
|
||||
用法:python tools/grant_risk_alert_read_permission.py
|
||||
用法:python tools/grant_risk_permissions.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
@@ -18,7 +18,7 @@ from app.infrastructure.db import SessionFactory
|
||||
from app.main import create_app
|
||||
from app.worker.runtime import WorkerRuntime
|
||||
|
||||
PRIVATE_KEY = Path("config/jwt/jwt-private.pem").read_text(encoding="utf-8")
|
||||
PRIVATE_KEY = Path(get_settings().jwt_private_key_path).read_text(encoding="utf-8")
|
||||
RISK_USER = "9002"
|
||||
AGENT_TYPE = "risk"
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from app.infrastructure.db import SessionFactory
|
||||
from app.main import create_app
|
||||
from app.worker.runtime import WorkerRuntime
|
||||
|
||||
PRIVATE_KEY = Path("config/jwt/jwt-private.pem").read_text(encoding="utf-8")
|
||||
PRIVATE_KEY = Path(get_settings().jwt_private_key_path).read_text(encoding="utf-8")
|
||||
RISK_USER = "9002"
|
||||
AGENT_TYPE = "risk"
|
||||
INTENT = "risk_overview"
|
||||
|
||||
Reference in New Issue
Block a user