完善风控登录权限和模型能力配置

This commit is contained in:
zhangshy
2026-09-12 14:43:05 +08:00
parent 026fec7514
commit 5634fdc023
7 changed files with 119 additions and 34 deletions
@@ -57,15 +57,18 @@ Agent 不能把截断结果表述成覆盖全部数据,也不能据此给出
以下内容属于环境准备或数据初始化,不是代码缺陷:
1. 执行 `python tools/grant_risk_permissions.py`,确认风控角色具备:
1. 先执行 `python tools/seed_test_rbac.py`,再执行
`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 配置和收件人范围符合演示要求。
2. 执行 `python tools/set_user_password.py --user 9002 --password 666666`,
为风控演示账号设置密码;登录使用 `risk_t / 666666`。
3. 执行 `python tools/publish_risk_agent_config.py`,确认奶龙风控智能助手的工具白名单和意图配置已发布。
4. 按主项目发布的迁移流程执行 Alembic 升级,确认 `trigger_rule_codes` 多值索引已生效。
5. 演示前准备足够的客户、交易、资金、持仓、登录和预警数据。
6. 使用前确认日报邮件开关、SMTP 配置和收件人范围符合演示要求。
## 当前保留限制
+23
View File
@@ -45,6 +45,7 @@ if str(PROJECT_ROOT) not in sys.path:
GRANT_SCRIPTS: tuple[tuple[str, str, str], ...] = (
("tools/grant_advisor_role.py", "ADVISOR_PERMISSIONS", "投顾"),
("tools/grant_customer_service_phase2_permissions.py", "PHASE2_PERMISSIONS", "客服二期"),
("tools/grant_risk_permissions.py", "PERMISSIONS", "风控"),
)
@@ -94,6 +95,28 @@ def collect_findings() -> list[str]:
if int(permission_id) not in seed_map:
problems.append(f"CUSTOMER_PERMISSIONS 引用了不存在的 id {permission_id}")
for permission_id in seed.RISK_PERMISSIONS:
if int(permission_id) not in seed_map:
problems.append(f"RISK_PERMISSIONS 引用了不存在的 id {permission_id}")
required_risk_codes = {
"risk:alert:read",
"risk:alert:write",
"risk:alert:scan",
"risk:report:mail",
}
risk_codes = {
seed_map[int(permission_id)]
for permission_id in seed.RISK_PERMISSIONS
if int(permission_id) in seed_map
}
missing_risk_codes = required_risk_codes - risk_codes
if missing_risk_codes:
problems.append(
"RISK_PERMISSIONS 缺少风控权限码:"
+ ", ".join(sorted(missing_risk_codes))
)
return problems
+9 -4
View File
@@ -29,13 +29,18 @@ import argparse
import asyncio
import sys
from datetime import UTC, datetime, timedelta
from pathlib import Path
from sqlalchemy import text
from app.core.contracts import RequestContext
from app.infrastructure.db import SessionFactory
from app.service.auth_service import hash_password
from app.service.identity_service import IdentityService
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from app.core.contracts import RequestContext # noqa: E402
from app.infrastructure.db import SessionFactory # noqa: E402
from app.service.auth_service import hash_password # noqa: E402
from app.service.identity_service import IdentityService # noqa: E402
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(errors="replace") # type: ignore[union-attr]
+20 -17
View File
@@ -46,12 +46,12 @@ if str(PROJECT_ROOT) not in sys.path:
from app.core.config import get_settings # noqa: E402
# (permission_code, resource, action, 授予的角色)
PERMISSIONS: tuple[tuple[str, str, str, tuple[str, ...]], ...] = (
("risk:alert:read", "risk", "alert", ("risk_operator", "admin")),
("risk:alert:write", "risk", "alert", ("risk_operator", "admin")),
("risk:alert:scan", "risk", "alert", ("risk_operator", "admin")),
("risk:report:mail", "risk", "report", ("risk_operator", "admin")),
# (permission_id, permission_code, resource, action, 授予的角色)
PERMISSIONS: tuple[tuple[int, str, str, str, tuple[str, ...]], ...] = (
(9047, "risk:alert:read", "risk", "alert", ("risk_operator", "admin")),
(9048, "risk:alert:write", "risk", "alert", ("risk_operator", "admin")),
(9049, "risk:alert:scan", "risk", "alert", ("risk_operator", "admin")),
(9050, "risk:report:mail", "risk", "report", ("risk_operator", "admin")),
)
DATA_SCOPE = "all"
@@ -75,7 +75,7 @@ async def main() -> int:
try:
cursor = connection.cursor()
now = dt.datetime.now(dt.UTC).replace(tzinfo=None)
for code, resource, action, roles in PERMISSIONS:
for permission_id, code, resource, action, roles in PERMISSIONS:
await cursor.execute(
"SELECT id FROM sys_permission WHERE permission_code = %s", (code,)
)
@@ -83,18 +83,21 @@ async def main() -> int:
if row is None:
await cursor.execute(
"INSERT INTO sys_permission"
" (permission_code, resource, `action`, data_scope, created_at, updated_at)"
" VALUES (%s, %s, %s, %s, %s, %s)",
(code, resource, action, DATA_SCOPE, now, now),
" (id, permission_code, resource, `action`, data_scope,"
" created_at, updated_at)"
" VALUES (%s, %s, %s, %s, %s, %s, %s)",
(permission_id, code, resource, action, DATA_SCOPE, now, now),
)
await cursor.execute(
"SELECT id FROM sys_permission WHERE permission_code = %s", (code,)
)
row = await cursor.fetchone()
print(f"[权限] 已创建 {code}(scope={DATA_SCOPE})")
else:
existing_id = int(row[0])
if existing_id != permission_id:
raise RuntimeError(
f"权限码 {code} 已存在但主键为 {existing_id},"
f"与约定主键 {permission_id} 不一致;请先执行 "
"python tools/seed_test_rbac.py"
)
print(f"[权限] {code} 已存在")
permission_id = int(row[0])
for role_code in roles:
await cursor.execute("SELECT id FROM sys_role WHERE role_code = %s", (role_code,))
@@ -122,5 +125,5 @@ async def main() -> int:
finally:
connection.close()
sys.exit(asyncio.run(main()))
if __name__ == "__main__":
sys.exit(asyncio.run(main()))
+13 -2
View File
@@ -5,11 +5,17 @@ from __future__ import annotations
import asyncio
import hashlib
import json
import sys
from datetime import UTC, datetime
from pathlib import Path
from sqlalchemy import text
from app.infrastructure.db import SessionFactory
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from app.infrastructure.db import SessionFactory # noqa: E402
ADMIN_USER_ID = 9003
ENDPOINT_CODE = "deepseek-flash"
@@ -53,7 +59,12 @@ async def seed() -> None:
status='active', reviewer_id=:admin_id, reviewed_at=:now, updated_at=:now
"""), {
"endpoint_code": ENDPOINT_CODE,
"capabilities": json.dumps(["chat", "intent_classification", "risk_answer"]),
"capabilities": json.dumps([
"chat",
"intent_classification",
"risk_answer",
"text_generation",
]),
"data_levels": json.dumps(["internal"]),
"admin_id": ADMIN_USER_ID,
"now": now,
+39 -4
View File
@@ -26,13 +26,19 @@ id BETWEEN 9001 AND 9099` 会删掉该号段内**所有**权限(包括别的
from __future__ import annotations
import asyncio
import sys
from datetime import UTC, datetime, timedelta
from pathlib import Path
from sqlalchemy import text
from app.core.contracts import RequestContext
from app.infrastructure.db import SessionFactory
from app.service.identity_service import IdentityService
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from app.core.contracts import RequestContext # noqa: E402
from app.infrastructure.db import SessionFactory # noqa: E402
from app.service.identity_service import IdentityService # noqa: E402
CUSTOMER_USER = "9001"
RISK_USER = "9002"
@@ -94,6 +100,14 @@ PERMISSIONS: tuple[tuple[int, str, str, str, str], ...] = (
(9044, "memory:candidate:confirm", "memory:candidate", "confirm", "self"),
(9045, "memory:candidate:review", "memory:candidate", "review", "all"),
(9046, "handover:read", "handover", "read", "all"),
# ---- 9047-9050:风控模块四个权限码 ----
# 风控 Service 和 Agent 工具都按这四个权限码失败关闭。此前依赖
# `grant_risk_permissions.py` 临时补种,重跑本脚本时会删除 9001-9099 号段内的
# 风控权限,导致登录成功后风控接口全部 403。这里纳入唯一权限定义源。
(9047, "risk:alert:read", "risk", "alert", "all"),
(9048, "risk:alert:write", "risk", "alert", "all"),
(9049, "risk:alert:scan", "risk", "alert", "all"),
(9050, "risk:report:mail", "risk", "report", "all"),
)
# 客户:业务侧自助能力(自己的会话、反馈、转人工、自己的记忆画像)。
@@ -104,7 +118,10 @@ CUSTOMER_PERMISSIONS = (
9044,
)
# 风控专员:业务侧只读 + 跨客户记忆 + 审计只读,不含配置写权限。
RISK_PERMISSIONS = (9001, 9002, 9003, 9010, 9011, 9012)
RISK_PERMISSIONS = (
9001, 9002, 9003, 9010, 9011, 9012,
9047, 9048, 9049, 9050,
)
# 平台管理员:管理面全套(配置发布四态 + 模型端点 + 审计)。
ADMIN_PERMISSIONS = tuple(row[0] for row in PERMISSIONS)
@@ -159,6 +176,24 @@ async def seed() -> None:
text("DELETE FROM sys_role_permission WHERE role_id IN (9001,9002,9003)")
)
await session.execute(text("DELETE FROM sys_user_role WHERE user_id IN (9001,9002,9003)"))
# 风控权限在旧环境中可能由 `grant_risk_permissions.py` 自动分配了任意主键。
# 先按 permission_code 清掉角色绑定和旧主键,保证本次能用固定号段重建;
# 否则 `risk:report:mail` 已存在于 9001-9099 之外时会触发 permission_code 唯一键冲突。
await session.execute(text("""
DELETE rp FROM sys_role_permission rp
JOIN sys_permission p ON p.id = rp.permission_id
WHERE p.permission_code IN (
'risk:alert:read', 'risk:alert:write',
'risk:alert:scan', 'risk:report:mail'
)
"""))
await session.execute(text("""
DELETE FROM sys_permission
WHERE permission_code IN (
'risk:alert:read', 'risk:alert:write',
'risk:alert:scan', 'risk:report:mail'
)
"""))
await session.execute(text("DELETE FROM sys_permission WHERE id BETWEEN 9001 AND 9099"))
await session.execute(text("DELETE FROM sys_role WHERE id IN (9001,9002,9003)"))
# 不再 DELETE sys_user:投顾域的 advisor_profile_tag 等表用 FK 引用它,库里有数据引用
+7 -2
View File
@@ -28,11 +28,16 @@ import argparse
import asyncio
import sys
from datetime import UTC, datetime
from pathlib import Path
from sqlalchemy import text
from app.infrastructure.db import SessionFactory
from app.service.auth_service import hash_password
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from app.infrastructure.db import SessionFactory # noqa: E402
from app.service.auth_service import hash_password # noqa: E402
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(errors="replace") # type: ignore[union-attr]