Files
group_xinghuo_jinrong/docs/course/jinrong-module-risk/index.html
T
zhanghongyu_0626 6f222f1c56 feat(course): Add course assembly script and module structure for advisor training
- Introduced `build_all.py` script to automate the assembly of course modules into a single `index.html` file.
- Created `index.html` for the main course overview, featuring a structured layout and navigation for various modules.
- Developed `_base.html` and `_footer.html` templates for the advisor module, ensuring consistent styling and structure.
- Added `build.sh` script for individual module assembly, enhancing modularity and ease of updates.
- Implemented multiple module HTML files detailing specific training scenarios and functionalities for advisors, including interactive elements and quizzes.

This update significantly enhances the course delivery framework, providing a comprehensive and interactive learning experience for advisors.
2026-09-09 23:22:59 +08:00

398 lines
22 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>风控监测 Agent 深潜 · 交互导览</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,600;12..96,700;12..96,800&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400;1,9..40,500&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
<style>
:root {
--color-accent: #D94F30;
--color-accent-hover: #B83E24;
--color-accent-light: #FCEEEA;
--color-accent-muted: #E87A62;
}
</style>
<script src="main.js" defer></script>
</head>
<body>
<nav class="nav" id="nav">
<div class="progress-bar" id="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
<div class="nav-inner">
<span class="nav-title">风控监测 Agent 深潜</span>
<div class="nav-dots" id="nav-dots" role="tablist">
<button class="nav-dot" data-target="module-1" data-tooltip="四条能力线" role="tab" aria-label="模块 1: 四条能力线"></button>
<button class="nav-dot" data-target="module-2" data-tooltip="交易链路" role="tab" aria-label="模块 2: 交易链路"></button>
<button class="nav-dot" data-target="module-3" data-tooltip="台账与对话" role="tab" aria-label="模块 3: 台账与对话"></button>
</div>
</div>
</nav>
<main id="main">
<section class="module" id="module-1">
<div class="module-inner">
<p class="eyebrow animate-in">模块 1 · 风控能力</p>
<h1 class="module-title animate-in">预警、适当性、AML<br>与模拟交易</h1>
<p class="module-lead animate-in">
风控专员 Demo <code>STAFF-30001</code> 带 <code>risk_officer</code> + <code>risk_demo</code> 角色:
能看预警台账、处置、跑 AML 扫描,还能在模拟交易页触发规则引擎。
本地登录页一键切换,改 JWT 角色后需<strong>重新登录</strong>。
</p>
<div class="screen animate-in">
<h2>四条能力线(REST + 对话 Tool 共用底座)</h2>
<div class="pattern-cards">
<div class="pattern-card">
<h3>预警台账 FR-4</h3>
<p><code>GET /api/risk/alerts</code> 分页查询;<code>POST .../handle</code> 人工处置(仅 risk_officer)。响应固定带 disclaimer。</p>
</div>
<div class="pattern-card">
<h3>适当性校验 FR-2</h3>
<p><code>POST /api/risk/suitability/check</code>:客户仅本人、理财师名下、风控全量。每次直调落 audit。</p>
</div>
<div class="pattern-card">
<h3>AML 扫描 FR-5</h3>
<p><code>POST /api/risk/aml/scan</code>:仅 risk_officer 触发全量扫描,生成 aml 类预警。</p>
</div>
<div class="pattern-card">
<h3>模拟交易 FR-1</h3>
<p><code>POST /api/simulate/trade</code>:risk_demo 或客户本人可提交;走 trade_gateway → 适当性 → 规则引擎。</p>
</div>
</div>
<div class="callout callout-accent">
<strong>鉴权与问数不同:</strong> 风控 REST 走 <code>get_auth_context</code>,前端 <code>risk.ts</code> 必须带 <code>X-Agent-Type: risk</code> + Bearer。缺头会 401 <code>AUTH_401_MISSING_AGENT_TYPE</code>。
</div>
</div>
<div class="screen animate-in">
<h2>群聊:风控专员打开预警页</h2>
<p>浏览器通过 riskHeaders 声明自己在敲风控窗:</p>
<div class="chat-window" id="chat-risk-m1">
<div class="chat-messages">
<div class="chat-message" data-msg="0" data-sender="browser" style="display:none">
<div class="chat-avatar" style="background:#D94F30">前</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#D94F30">RiskAlertsPage</span>
<p>GET /api/risk/alerts · Bearer + X-Agent-Type: risk</p>
</div>
</div>
<div class="chat-message" data-msg="1" data-sender="api" style="display:none">
<div class="chat-avatar" style="background:#E87A62">API</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#E87A62">deps.get_auth_context</span>
<p>验 JWT + 矩阵:STAFF-30001 risk_officer 放行</p>
</div>
</div>
<div class="chat-message" data-msg="2" data-sender="repo" style="display:none">
<div class="chat-avatar" style="background:#B83E24">库</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#B83E24">RiskRepository</span>
<p>list_alerts(status=pending_review) → items + total + disclaimer</p>
</div>
</div>
<div class="chat-message" data-msg="3" data-sender="ui" style="display:none">
<div class="chat-avatar" style="background:#D4A843">页</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#D4A843">Table + Modal</span>
<p>展示预警列表;点处置调 handleAlert → POST .../handle</p>
</div>
</div>
</div>
<div class="chat-typing" id="chat-risk-m1-typing" style="display:none">
<div class="chat-avatar" id="chat-risk-m1-typing-avatar">…</div>
<div class="chat-typing-dots"><span class="typing-dot"></span><span class="typing-dot"></span><span class="typing-dot"></span></div>
</div>
<div class="chat-controls">
<button class="btn chat-next-btn">下一条</button>
<button class="btn chat-all-btn">全部播放</button>
<button class="btn chat-reset-btn">重播</button>
<span class="chat-progress"></span>
</div>
</div>
</div>
<div class="screen animate-in">
<div class="translation-block">
<div class="translation-code">
<span class="translation-label">CODE · simulate.py 鉴权</span>
<pre><code><span class="code-line"><span class="code-keyword">if not</span> (auth.has_role(<span class="code-string">"risk_demo"</span>)</span>
<span class="code-line"> <span class="code-keyword">or</span> (auth.is_customer()</span>
<span class="code-line"> <span class="code-keyword">and</span> auth.customer_id == req.customer_id)):</span>
<span class="code-line"> deny(auth, <span class="code-string">"AUTH_403_ROLE"</span>, ...)</span>
<span class="code-line"><span class="code-keyword">return</span> submit_trade(req.model_dump(), actor_id=auth.actor_id)</span></code></pre>
</div>
<div class="translation-english">
<span class="translation-label">白话</span>
<div class="translation-lines">
<p class="tl">模拟交易不是谁都能点:要么 risk_demo 演示号,要么客户给自己下单。</p>
<p class="tl">STAFF-30001 有 risk_demo 权限,可在 RiskSimulatePage 代客触发规则。</p>
<p class="tl">越权会 deny 并写审计,不会悄悄放行。</p>
</div>
</div>
</div>
<div class="quiz-container" id="quiz-risk-m1">
<div class="quiz-question-block"
data-correct="option-b"
data-explanation-right="risk_demo 是 STAFF-30001 的演示权限,允许在模拟交易页提交 trade 请求。"
data-explanation-wrong="普通 staff 没有 risk_demo;模拟交易专门给演示账号或客户本人。">
<h3 class="quiz-question">STAFF-30001 能在模拟交易页提交申购吗?</h3>
<div class="quiz-options">
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>不能,只有客户本人</span>
</button>
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>能,因为有 risk_demo 角色</span>
</button>
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>能,任何 staff 都行</span>
</button>
</div>
<div class="quiz-feedback"></div>
</div>
<button class="quiz-check-btn" onclick="checkQuiz('quiz-risk-m1')">检查答案</button>
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-risk-m1')">重做</button>
</div>
</div>
</div>
</section>
<section class="module" id="module-2">
<div class="module-inner">
<p class="eyebrow animate-in">模块 2 · 模拟交易链路</p>
<h1 class="module-title animate-in">一笔演示交易<br>怎么长出预警单?</h1>
<p class="module-lead animate-in">
<code>RiskSimulatePage</code> 提交表单 → <code>simulate.py</code> 验权 → <code>trade_gateway</code> 写库 →
<code>risk_engine</code> 扫规则 → 聚合进 <code>risk_alert</code>。
像演示用的「假收银台」:钱是假的,但风控流程是真的。
</p>
<div class="screen animate-in">
<h2>四段链路(动画)</h2>
<div class="flow-animation" data-steps='[
{"highlight":"flow-actor-1","label":"RiskSimulatePage:submitSimulateTrade(token, \"risk\", body)"},
{"highlight":"flow-actor-2","label":"POST /api/simulate/trade → 鉴权 risk_demo 或客户本人","packet":true,"from":"1","to":"2"},
{"highlight":"flow-actor-3","label":"trade_gateway:R-02 适当性 → INSERT core_trade(唯一写 Core 路径)","packet":true,"from":"2","to":"3"},
{"highlight":"flow-actor-4","label":"risk_engine 扫描 → 写 risk_alert(大额/集中度等)","packet":true,"from":"3","to":"4"},
{"highlight":"flow-actor-1","label":"台账 GET /api/risk/alerts 可见 + 固定 disclaimer","packet":true,"from":"4","to":"1"}
]'>
<div class="flow-actors">
<div class="flow-actor" id="flow-actor-1"><span class="flow-actor-icon">🧪</span><span>模拟交易 UI</span></div>
<div class="flow-actor" id="flow-actor-2"><span class="flow-actor-icon">🚪</span><span>simulate.py</span></div>
<div class="flow-actor" id="flow-actor-3"><span class="flow-actor-icon">⚙</span><span>trade_gateway</span></div>
<div class="flow-actor" id="flow-actor-4"><span class="flow-actor-icon">🛡</span><span>risk_engine</span></div>
</div>
<p class="flow-step-label">Preset A-3:CUST-3001 · 50 万 → 大额预警 · Preset A-1:适当性阻断</p>
<div class="flow-controls">
<button class="btn flow-next-btn">下一步</button>
<button class="btn flow-reset-btn">重来</button>
</div>
</div>
</div>
<div class="screen animate-in">
<h2>代码地图</h2>
<div class="file-tree animate-in">
<div class="tree-item tree-folder">app/api/simulate.py</div>
<div class="tree-item tree-indent">薄路由:鉴权 + 调 submit_trade</div>
<div class="tree-item tree-folder">app/gateway/trade_gateway.py</div>
<div class="tree-item tree-indent">适当性 → 写 core_trade → 调 risk 引擎</div>
<div class="tree-item tree-folder">app/service/risk/</div>
<div class="tree-item tree-indent">risk_engine.py · alert_service.py · 规则聚合</div>
<div class="tree-item tree-folder">web/src/pages/risk/RiskSimulatePage.tsx</div>
<div class="tree-item tree-indent">预设 A-1 / A-3 一键填表</div>
</div>
</div>
<div class="screen animate-in">
<div class="translation-block">
<div class="translation-code">
<span class="translation-label">CODE · simulate.py</span>
<pre><code><span class="code-line"><span class="code-keyword">if not</span> (auth.has_role(<span class="code-string">"risk_demo"</span>)</span>
<span class="code-line"> <span class="code-keyword">or</span> (auth.is_customer() <span class="code-keyword">and</span></span>
<span class="code-line"> auth.customer_id == req.customer_id)):</span>
<span class="code-line"> deny(auth, <span class="code-string">"AUTH_403_ROLE"</span>, ...)</span>
<span class="code-line"><span class="code-keyword">return</span> submit_trade(req.model_dump(), ...)</span></code></pre>
</div>
<div class="translation-english">
<span class="translation-label">白话</span>
<div class="translation-lines">
<p class="tl">能点「提交模拟交易」的只有两类人:带 risk_demo 的风控演示号,或客户本人买自己的单。</p>
<p class="tl">STAFF-30001 在 jwt_service 里配了 risk_demo,所以能造 A-3 大额演示数据。</p>
</div>
</div>
</div>
<div class="quiz-container" id="quiz-risk-m2">
<div class="quiz-question-block"
data-correct="option-c"
data-explanation-right="trade_gateway 是唯一允许向 jinrong_core 写 INSERT 的路径(模拟成交);Agent 画像不能直接改 Core 正式字段。"
data-explanation-wrong="问数和其他 Agent 对 Core 是只读;只有网关模拟交易可写 core_trade。">
<h3 class="quiz-question">Demo 里谁可以把新成交写进 core_trade?</h3>
<div class="quiz-options">
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>analyst_agent 问数 SQL</span>
</button>
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>风控对话 Tool</span>
</button>
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>trade_gateway(经 simulate/trade)</span>
</button>
</div>
<div class="quiz-feedback"></div>
</div>
<button class="quiz-check-btn" onclick="checkQuiz('quiz-risk-m2')">检查答案</button>
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-risk-m2')">重做</button>
</div>
</div>
</div>
</section>
<section class="module" id="module-3">
<div class="module-inner">
<p class="eyebrow animate-in">模块 3 · REST 与对话 Tool</p>
<h1 class="module-title animate-in">台账 REST 要带 risk 头<br>对话靠五只 Tool</h1>
<p class="module-lead animate-in">
风控<span class="term" data-definition="REST:用 URL + HTTP 方法(GET/POST)访问的后端接口,浏览器和脚本都能调。">REST</span>
走 <code>get_auth_context</code>,前端必须带 <code>X-Agent-Type: risk</code>。
风控对话则通过 <code>chat_tools</code> 注册五只只读 Tool + <code>query_agent_behavior</code>,由 <code>tool_service</code> 统一分发。
</p>
<div class="screen animate-in">
<h2>预警 REST 两端点</h2>
<div class="pattern-cards">
<div class="pattern-card">
<h3>GET /api/risk/alerts</h3>
<p>risk_officer / risk_manager 全量;compliance 强制只看 aml;响应含 <code>disclaimer</code></p>
</div>
<div class="pattern-card">
<h3>POST /api/risk/alerts/{id}/handle</h3>
<p>仅 risk_officer;handler_result 三枚举 + 审计;409 状态冲突</p>
</div>
</div>
<div class="callout callout-warning">
<strong>曾踩过的坑:</strong> <code>web/src/api/risk.ts</code> 若漏 <code>X-Agent-Type: risk</code>,JWT 正确也 401。
问数 API 则相反——不要带这个头。
</div>
</div>
<div class="screen animate-in">
<h2>chat_tools 注册表(C1 + 扩展)</h2>
<div class="file-tree animate-in">
<div class="tree-item tree-folder">app/service/risk/chat_tools.py</div>
<div class="tree-item tree-indent">alert_query — 客户或全量待审预警</div>
<div class="tree-item tree-indent">customer_context — L0 + L3 + 待审预警</div>
<div class="tree-item tree-indent">suitability_check — 只读校验 + 审计落库</div>
<div class="tree-item tree-indent">aml_lookup — 名单匹配</div>
<div class="tree-item tree-indent">query_overdue_alerts — 超期未处置(FR-9)</div>
<div class="tree-item tree-indent">query_agent_behavior — 代理人行为链(FR-10)</div>
</div>
<p>红线:Tool 只读,不生成预警单、不处置、不改正式风险等级。</p>
</div>
<div class="screen animate-in">
<h2>群聊:风控问「今天多少待审预警」</h2>
<div class="chat-window" id="chat-risk-m3">
<div class="chat-messages">
<div class="chat-message" data-msg="0" data-sender="browser" style="display:none">
<div class="chat-avatar" style="background:#D94F30">前</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#D94F30">RiskChatPage</span>
<p>POST /api/chat/stream · X-Agent-Type: risk</p>
</div>
</div>
<div class="chat-message" data-msg="1" data-sender="chat" style="display:none">
<div class="chat-avatar" style="background:#E87A62">API</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#E87A62">tool_service</span>
<p>意图命中「待审预警」→ alert_query(customer_id 空缺=全量)</p>
</div>
</div>
<div class="chat-message" data-msg="2" data-sender="tool" style="display:none">
<div class="chat-avatar" style="background:#B83D24">Tool</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#B83D24">alert_query</span>
<p>RiskRepository.list_alerts → 今日新增口径 + alert_id 列表</p>
</div>
</div>
<div class="chat-message" data-msg="3" data-sender="llm" style="display:none">
<div class="chat-avatar" style="background:#D4A843">AI</div>
<div class="chat-bubble">
<span class="chat-sender" style="color:#D4A843">DeepSeek</span>
<p>组织自然语言;可追问 query_agent_behavior 查 STAFF 行为链</p>
</div>
</div>
</div>
<div class="chat-typing" id="chat-risk-m3-typing" style="display:none">
<div class="chat-avatar" id="chat-risk-m3-typing-avatar">…</div>
<div class="chat-typing-dots"><span class="typing-dot"></span><span class="typing-dot"></span><span class="typing-dot"></span></div>
</div>
<div class="chat-controls">
<button class="btn chat-next-btn">下一条</button>
<button class="btn chat-all-btn">全部播放</button>
<button class="btn chat-reset-btn">重播</button>
<span class="chat-progress"></span>
</div>
</div>
</div>
<div class="screen animate-in">
<div class="translation-block">
<div class="translation-code">
<span class="translation-label">前端 · risk.ts + simulate</span>
<pre><code><span class="code-line">apiFetch(<span class="code-string">'/api/risk/alerts'</span>, {</span>
<span class="code-line"> token, agentType: <span class="code-string">'risk'</span>, <span class="code-comment">// 必须</span></span>
<span class="code-line">})</span>
<span class="code-line"><span class="code-comment">// RiskSimulatePage → /api/simulate/trade 同样带 risk</span></span></code></pre>
</div>
<div class="translation-english">
<span class="translation-label">白话</span>
<div class="translation-lines">
<p class="tl">台账页、模拟交易页、风控对话——凡走 get_auth_context 的都要声明 risk 线。</p>
<p class="tl">处置预警用 handle REST;日常问数用对话 Tool,两者不要混接口。</p>
</div>
</div>
</div>
<div class="quiz-container" id="quiz-risk-m3">
<div class="quiz-question-block"
data-correct="option-a"
data-explanation-right="query_agent_behavior 查 audit/行为链,支持 agent_id 过滤;是 FR-10 专用 Tool。"
data-explanation-wrong="customer_context 是客户档案+L3;行为链是独立 Tool。">
<h3 class="quiz-question">风控对话里问「代理人 STAFF-Q 有没有越权记录」,应触发哪个 Tool?</h3>
<div class="quiz-options">
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>query_agent_behavior</span>
</button>
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>customer_context</span>
</button>
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
<div class="quiz-option-radio"></div><span>aml_lookup</span>
</button>
</div>
<div class="quiz-feedback"></div>
</div>
<button class="quiz-check-btn" onclick="checkQuiz('quiz-risk-m3')">检查答案</button>
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-risk-m3')">重做</button>
</div>
<p style="margin-top:2rem; font-size:0.9rem; color: var(--color-text-muted);">
仓库现状(2026-09):STAFF-30001 含 risk_demo;<code>python -m pytest</code> → 774 passed。
</p>
</div>
</div>
</section>
</main>
</body>
</html>