修复风控时间口径与Agent截断提示

This commit is contained in:
zhangshy
2026-09-11 15:25:45 +08:00
parent 5102eaac64
commit d2cdbbac01
24 changed files with 318 additions and 27 deletions
@@ -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 时,必须明确说明当前证据不完整,不能按全量证据下结论;"
"只有结果明确完整时,才能表述为覆盖全部记录。"
)
+3 -2
View File
@@ -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),
+10 -8
View File
@@ -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 实际对话、证据查询、通知查询和日报邮件分别完成一次人工联调。
+2
View File
@@ -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)
+1 -1
View File
@@ -30,7 +30,7 @@
幂等:权限与绑定都已存在时直接跳过。
用法:python tools/grant_risk_alert_read_permission.py
用法:python tools/grant_risk_permissions.py
"""
import asyncio
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"