- Added `status` query parameter to `list_sessions_api` for filtering sessions by their status (active/closed). - Introduced `close_all_sessions_api` endpoint to allow users to close all active sessions for the current actor. - Updated `SessionRepository` to support status filtering in session listing and implemented logic for closing active sessions. - Improved Redis connection settings for better performance and reliability. This update enhances the chat functionality by providing more control over session management, improving user experience and system efficiency.
437 lines
24 KiB
HTML
437 lines
24 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>前端 web 深潜 · 交互导览</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: #132B3A;
|
||
--color-accent-hover: #0f2230;
|
||
--color-accent-light: #E8EEF1;
|
||
--color-accent-muted: #5a6b75;
|
||
}
|
||
</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">前端 web 深潜</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="Chat 鉴权" role="tab" aria-label="模块 2: Chat 鉴权"></button>
|
||
<button class="nav-dot" data-target="module-3" data-tooltip="本地调试" role="tab" aria-label="模块 3: 本地调试"></button>
|
||
<button class="nav-dot" data-target="module-4" data-tooltip="未接线" role="tab" aria-label="模块 4: 未接线"></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">四角色路由、HashRouter<br>与 Demo 账号</h1>
|
||
<p class="module-lead animate-in">
|
||
前端在 <code>web/</code>,用 React +
|
||
<span class="term" data-definition="HashRouter = URL 里 # 后面的路径由浏览器本地切换,不请求服务器新页面,适合静态部署。">HashRouter</span>
|
||
做四角色工作台。登录后按角色跳进不同「家」——改路由时先对 <code>App.tsx</code> 和 <code>demoAccounts.ts</code>。
|
||
</p>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>四角色默认入口</h2>
|
||
<div class="badge-list">
|
||
<div class="badge-item"><span class="badge-code">CUST-9527</span><span class="badge-desc">客户 → /app/customer/home</span></div>
|
||
<div class="badge-item"><span class="badge-code">STAFF-10086</span><span class="badge-desc">理财师 → /app/advisor/home</span></div>
|
||
<div class="badge-item"><span class="badge-code">STAFF-20001</span><span class="badge-desc">分析员 → /app/analyst/home</span></div>
|
||
<div class="badge-item"><span class="badge-code">STAFF-30001</span><span class="badge-desc">风控 → /app/risk/home</span></div>
|
||
</div>
|
||
<p>登录页一键 Demo:<code>LoginPage</code> 调 <code>POST /api/auth/login</code> 拿 JWT,<code>saveAuth</code> 写入 localStorage,再 <code>navigate(defaultRoute)</code>。</p>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>路由树(<code>App.tsx</code>)</h2>
|
||
<div class="pattern-cards">
|
||
<div class="pattern-card">
|
||
<h3>customer/*</h3>
|
||
<p>home、profile、holdings、trades、chat。持仓/流水走平台 API,对话走 ChatPanel。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>advisor/*</h3>
|
||
<p>home、customers、chat。顾问助手 SSE,可指定 customer_id 查名下客户。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>analyst/*</h3>
|
||
<p>home、analytics/query(问数)、analytics/assets、analytics/chat。问数走 /api/analyst/*。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>risk/*</h3>
|
||
<p>home、alerts、simulate、chat。台账 REST + 风控对话,JWT 须带 risk 角色。</p>
|
||
</div>
|
||
</div>
|
||
<div class="callout callout-accent">
|
||
<strong>HashRouter 为什么?</strong> 构建产物是纯静态文件,<code>#/app/customer/home</code> 不依赖服务端路由重写,双击 <code>dist/index.html</code> 也能跑(API 仍要 proxy 或同域)。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<div class="translation-block">
|
||
<div class="translation-code">
|
||
<span class="translation-label">main.tsx + App.tsx</span>
|
||
<pre><code><span class="code-line"><span class="code-comment">// main.tsx</span></span>
|
||
<span class="code-line"><HashRouter></span>
|
||
<span class="code-line"> <AppRoutes /></span>
|
||
<span class="code-line"></HashRouter></span>
|
||
<span class="code-line"></span>
|
||
<span class="code-line"><span class="code-comment">// demoAccounts.ts</span></span>
|
||
<span class="code-line">defaultRoute: <span class="code-string">'#/app/customer/home'</span></span>
|
||
<span class="code-line"></span>
|
||
<span class="code-line"><span class="code-comment">// App.tsx · RequireAuth</span></span>
|
||
<span class="code-line"><span class="code-keyword">if</span> (!auth) <span class="code-keyword">return</span> <Navigate to=<span class="code-string">"/login"</span> /></span></code></pre>
|
||
</div>
|
||
<div class="translation-english">
|
||
<span class="translation-label">白话</span>
|
||
<div class="translation-lines">
|
||
<p class="tl">整站包在 HashRouter 里,路径变化不刷新整页。</p>
|
||
<p class="tl">每个 Demo 账号绑好「登录后跳哪」——带 # 前缀,和 Hash 路由一致。</p>
|
||
<p class="tl">/app 下子路由都要过 RequireAuth:没 token 踢回登录页。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="quiz-container" id="quiz-frontend-m1">
|
||
<div class="quiz-question-block"
|
||
data-correct="option-b"
|
||
data-explanation-right="STAFF-20001 登录后 defaultRoute 是 #/app/analyst/home,见 demoAccounts.ts。"
|
||
data-explanation-wrong="四角色各有独立 home,分析员不会进 customer 或 risk 路径。">
|
||
<h3 class="quiz-question">分析员 Demo 登录成功,默认打开哪条路径?</h3>
|
||
<div class="quiz-options">
|
||
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>/app/customer/home</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>/app/analyst/home</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>/app/risk/alerts</span>
|
||
</button>
|
||
</div>
|
||
<div class="quiz-feedback"></div>
|
||
</div>
|
||
<button class="quiz-check-btn" onclick="checkQuiz('quiz-frontend-m1')">检查答案</button>
|
||
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-frontend-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">ChatPanel / useChatPanel:<br>三条 API 线的头不一样</h1>
|
||
<p class="module-lead animate-in">
|
||
对话 UI 统一用 <code>ChatPanel</code> + <code>useChatPanel</code>,但后端入口分三条:
|
||
<code>/api/chat</code>(四 Agent)、<code>/api/analyst/*</code>(问数平台)、风控台账 REST。
|
||
指挥 AI 接新页面时,<strong>先对表再写 headers</strong>。
|
||
</p>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>鉴权差异对照</h2>
|
||
<table style="width:100%; border-collapse:collapse; margin: 1.5rem 0; font-size: 0.95rem;">
|
||
<thead><tr><th>前端模块</th><th>API</th><th>Authorization</th><th>X-Agent-Type</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>api/chat.ts</code></td><td>/api/chat, /stream, sessions</td><td>Bearer</td><td><strong>必须</strong>(与 prop agentType 一致)</td></tr>
|
||
<tr><td><code>api/analyst.ts</code></td><td>/api/analyst/chat 等</td><td>Bearer(apiFetch)</td><td><strong>不要</strong></td></tr>
|
||
<tr><td><code>api/risk.ts</code></td><td>/api/risk/*</td><td>Bearer</td><td><strong>必须 risk</strong></td></tr>
|
||
<tr><td>持仓/产品页</td><td>/api/customers/* 等</td><td>Bearer</td><td>不要</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<div class="callout callout-warning">
|
||
<strong>真实 bug:</strong> <code>risk.ts</code> 曾漏 <code>X-Agent-Type: risk</code>,JWT 正确也 401。接新风控页先 grep 这个头。
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>数据流:理财师打开顾问助手</h2>
|
||
<div class="flow-animation" data-steps='[
|
||
{"highlight":"flow-actor-1","label":"ChatPanel(agentType=advisor, mode=stream)"},
|
||
{"highlight":"flow-actor-2","label":"useChatPanel → listChatSessions(token, advisor)","packet":true,"from":"1","to":"2"},
|
||
{"highlight":"flow-actor-3","label":"chat.ts:Bearer + X-Agent-Type: advisor","packet":true,"from":"2","to":"3"},
|
||
{"highlight":"flow-actor-4","label":"对话 HTTP 入口(chat.py):get_auth_context + 准入矩阵","packet":true,"from":"3","to":"4"},
|
||
{"highlight":"flow-actor-1","label":"SSE 流式 delta → UI 逐字显示","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>ChatPanel</span></div>
|
||
<div class="flow-actor" id="flow-actor-2"><span class="flow-actor-icon">🪝</span><span>useChatPanel</span></div>
|
||
<div class="flow-actor" id="flow-actor-3"><span class="flow-actor-icon">📡</span><span>api/chat.ts</span></div>
|
||
<div class="flow-actor" id="flow-actor-4"><span class="flow-actor-icon">🚪</span><span>对话 HTTP 入口(chat.py)</span></div>
|
||
</div>
|
||
<p class="flow-step-label">对比:问数走 analyst.ts,无 X-Agent-Type</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">
|
||
<div class="translation-block">
|
||
<div class="translation-code">
|
||
<span class="translation-label">chat.ts vs analyst.ts</span>
|
||
<pre><code><span class="code-line"><span class="code-comment">// chat.ts — 对话线</span></span>
|
||
<span class="code-line"><span class="code-keyword">function</span> <span class="code-function">chatHeaders</span>(token, agentType) {</span>
|
||
<span class="code-line"> <span class="code-keyword">return</span> {</span>
|
||
<span class="code-line"> Authorization: <span class="code-string">`Bearer ${token}`</span>,</span>
|
||
<span class="code-line"> <span class="code-string">'X-Agent-Type'</span>: agentType,</span>
|
||
<span class="code-line"> }</span>
|
||
<span class="code-line">}</span>
|
||
<span class="code-line"></span>
|
||
<span class="code-line"><span class="code-comment">// analyst.ts — 问数平台线</span></span>
|
||
<span class="code-line">apiFetch(<span class="code-string">'/api/analyst/chat'</span>, { method: <span class="code-string">'POST'</span>, token, body })</span>
|
||
<span class="code-line"><span class="code-comment">// apiFetch 只加 Authorization,不加 X-Agent-Type</span></span></code></pre>
|
||
</div>
|
||
<div class="translation-english">
|
||
<span class="translation-label">白话</span>
|
||
<div class="translation-lines">
|
||
<p class="tl">Chat 相关请求:token 证明身份,agentType 证明走哪条 Agent 线。</p>
|
||
<p class="tl">useChatPanel 把 agentType 从页面 prop 一路传到每次 list/send/close。</p>
|
||
<p class="tl">问数用 apiFetch 通用封装,后端走 get_platform_auth_context,不需要 Agent 头。</p>
|
||
<p class="tl">customer 线 mode 常用 sync(后端 LangGraph 整图跑完再推);advisor/risk 用 stream SSE。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="quiz-container" id="quiz-frontend-m2">
|
||
<div class="quiz-question-block"
|
||
data-correct="option-c"
|
||
data-explanation-right="风控对话页 ChatPanel agentType=risk,chat.ts 自动带 X-Agent-Type: risk。"
|
||
data-explanation-wrong="风控 Chat 走 /api/chat,不是 /api/risk;但 REST 台账才用 risk.ts。">
|
||
<h3 class="quiz-question">RiskChatPage 发消息,请求头应长什么样?</h3>
|
||
<div class="quiz-options">
|
||
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>只要 Bearer,和问数一样</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>Bearer + X-Agent-Type: analyst</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>Bearer + X-Agent-Type: risk</span>
|
||
</button>
|
||
</div>
|
||
<div class="quiz-feedback"></div>
|
||
</div>
|
||
<button class="quiz-check-btn" onclick="checkQuiz('quiz-frontend-m2')">检查答案</button>
|
||
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-frontend-m2')">重做</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section class="module" id="module-3">
|
||
<div class="module-inner">
|
||
<p class="eyebrow animate-in">模块 3 · 本地调试</p>
|
||
<h1 class="module-title animate-in">npm run dev、Vite proxy<br>与 401 排障</h1>
|
||
<p class="module-lead animate-in">
|
||
前端 dev 服务器跑在 <code>5173</code>,API 在 <code>8000</code>。
|
||
<span class="term" data-definition="Vite proxy = 开发时把 /api 请求转发到后端,浏览器以为 API 和页面同域,避免跨域 CORS 麻烦。">Vite proxy</span>
|
||
帮你转发——没起后端或头带错,Network 面板一眼 401。
|
||
</p>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>启动顺序</h2>
|
||
<div class="numbered-steps">
|
||
<div class="step-card">
|
||
<span class="step-num">1</span>
|
||
<div><strong>后端</strong> · 应用入口(<code>main.py</code>):<code>uvicorn app.main:app --reload --port 8000</code>(Core 模拟库 + Redis 6380 按需)</div>
|
||
</div>
|
||
<div class="step-card">
|
||
<span class="step-num">2</span>
|
||
<div><strong>前端</strong>:<code>cd web && npm run dev</code> → 打开 <code>http://localhost:5173</code></div>
|
||
</div>
|
||
<div class="step-card">
|
||
<span class="step-num">3</span>
|
||
<div><strong>登录</strong>:点 Demo 账号 → 看 localStorage 里 token → 再点各角色菜单</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<div class="translation-block">
|
||
<div class="translation-code">
|
||
<span class="translation-label">vite.config.ts</span>
|
||
<pre><code><span class="code-line">server: {</span>
|
||
<span class="code-line"> port: <span class="code-number">5173</span>,</span>
|
||
<span class="code-line"> proxy: {</span>
|
||
<span class="code-line"> <span class="code-string">'/api'</span>: {</span>
|
||
<span class="code-line"> target: <span class="code-string">'http://127.0.0.1:8000'</span>,</span>
|
||
<span class="code-line"> changeOrigin: <span class="code-keyword">true</span>,</span>
|
||
<span class="code-line"> },</span>
|
||
<span class="code-line"> },</span>
|
||
<span class="code-line">},</span></code></pre>
|
||
</div>
|
||
<div class="translation-english">
|
||
<span class="translation-label">白话</span>
|
||
<div class="translation-lines">
|
||
<p class="tl">浏览器请求 localhost:5173/api/... 时,Vite 偷偷转给 8000 端口。</p>
|
||
<p class="tl">前端代码里写相对路径 /api 即可,不用硬编码 :8000。</p>
|
||
<p class="tl">生产构建没有 proxy,要 Nginx 或同域部署反代。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>常见 401 速查</h2>
|
||
<div class="pattern-cards">
|
||
<div class="pattern-card">
|
||
<h3>AUTH_401_MISSING_BEARER</h3>
|
||
<p>没登录或 token 过期 → 重新点 Demo 登录;改过后端角色也要重登拿新 JWT。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>AUTH_401_MISSING_AGENT_TYPE</h3>
|
||
<p>走了 /api/chat 但没带 X-Agent-Type → 查 ChatPanel 的 agentType 和 chat.ts headers。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>AUTH_403_AGENT_MISMATCH</h3>
|
||
<p>JWT 角色和 X-Agent-Type 不配,如客户 token 却带 advisor → 换账号或改 prop。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>连不上 API</h3>
|
||
<p>8000 没起或 proxy 失效 → curl http://127.0.0.1:8000/health 先确认后端活着。</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="quiz-container" id="quiz-frontend-m3">
|
||
<div class="quiz-question-block"
|
||
data-correct="option-b"
|
||
data-explanation-right="持仓页走平台 API,只需 Bearer;缺的是 X-Agent-Type 才会在 chat 线 401。"
|
||
data-explanation-wrong="平台读接口不要 X-Agent-Type;401 missing bearer 才是没 token。">
|
||
<h3 class="quiz-question">客户持仓页 401,响应 AUTH_401_MISSING_AGENT_TYPE,最可能原因是?</h3>
|
||
<div class="quiz-options">
|
||
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>平台 API 忘了带 X-Agent-Type(正常不该带)</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>请求误打到 /api/chat 或混用了 chat 封装</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>vite proxy 端口写错</span>
|
||
</button>
|
||
</div>
|
||
<div class="quiz-feedback"></div>
|
||
</div>
|
||
<div class="quiz-question-block"
|
||
data-correct="option-a"
|
||
data-explanation-right="对。/api → 127.0.0.1:8000,前端 dev 标准配置。"
|
||
data-explanation-wrong="proxy target 是 8000 不是 5173;5173 是 Vite 自己。">
|
||
<h3 class="quiz-question">npm run dev 时,/api/chat 实际打到哪?</h3>
|
||
<div class="quiz-options">
|
||
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>http://127.0.0.1:8000/api/chat</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>http://127.0.0.1:5173/api/chat(Vite 自己处理)</span>
|
||
</button>
|
||
</div>
|
||
<div class="quiz-feedback"></div>
|
||
</div>
|
||
<button class="quiz-check-btn" onclick="checkQuiz('quiz-frontend-m3')">检查答案</button>
|
||
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-frontend-m3')">重做</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section class="module" id="module-4">
|
||
<div class="module-inner">
|
||
<p class="eyebrow animate-in">模块 4 · 未接线</p>
|
||
<h1 class="module-title animate-in">菜单有 ≠ API 已接<br>Chat 还有 B 方案三端点</h1>
|
||
<p class="module-lead animate-in">
|
||
前端 P0 主链路已真接(含风控三页 · 问数标签 · Dashboard hooks),但仍有<strong>分析对话占位、看板无钻取</strong>等缝。
|
||
开发态若 Dashboard「先闪错再正常」,多半是 StrictMode 双请求——已用 <code>useAsyncSequence</code> 丢弃过期响应。
|
||
</p>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>仍开放 / 已修清单(2026-09-10)</h2>
|
||
<div class="pattern-cards">
|
||
<div class="pattern-card">
|
||
<h3>风控 UI</h3>
|
||
<p>台账筛选 · 适当性校验 · AML 扫描页已接 REST;高级运营能力仍简版。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>问数</h3>
|
||
<p><code>AnalystChatShell</code> 占位 · 真问数在 <code>AnalystQueryPage</code>(模板/缓存标签 + dashboard/assets)。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>竞态修复</h3>
|
||
<p><code>useAsyncSequence</code> 用于 Dashboard hooks、ChatPanel、风控/客户列表页等。</p>
|
||
</div>
|
||
<div class="pattern-card">
|
||
<h3>游客</h3>
|
||
<p><code>VisitorChatWidget</code> 首页试聊 · 与登录 ChatPanel 不同 API。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>Chat 方案 B:三端点 + SSE 注意点</h2>
|
||
<div class="translation-block">
|
||
<div class="translation-code">
|
||
<span class="translation-label">会话 API</span>
|
||
<pre><code><span class="code-line">GET /api/chat/sessions</span>
|
||
<span class="code-line">GET /api/chat/sessions/{id}/messages</span>
|
||
<span class="code-line">POST /api/chat/sessions/{id}/close</span>
|
||
<span class="code-line">POST /api/chat/stream → customer SSE</span></code></pre>
|
||
</div>
|
||
<div class="translation-english">
|
||
<span class="translation-label">白话</span>
|
||
<div class="translation-lines">
|
||
<p class="tl">列表/历史/关闭与发消息共用同一套鉴权(Bearer + X-Agent-Type)。</p>
|
||
<p class="tl">SSE 无心跳:断连可能留下半空回合——排障时查 session 表 + 是否 uvicorn 重启。</p>
|
||
<p class="tl">HashRouter:URL 带 <code>#</code>,生产 Nginx 要配 SPA fallback。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screen animate-in">
|
||
<h2>改 URL 进别人工作台</h2>
|
||
<p><code>RequireAuth</code> 只验「有没有 token」,<strong>不</strong>验「这个角色能不能进 /app/risk」。菜单隐藏 ≠ 安全——后端 <code>AGENT_ACCESS_MATRIX</code> 和平台 RBAC 才拒 403。</p>
|
||
|
||
<div class="quiz-container" id="quiz-frontend-m4">
|
||
<div class="quiz-question-block"
|
||
data-correct="option-b"
|
||
data-explanation-right="对。前端藏菜单是 UX;越权要靠后端模块鉴权(deps.py)矩阵和 SessionGuard 403。"
|
||
data-explanation-wrong="仅改前端路由守卫不够,必须对后端入口断言。">
|
||
<h3 class="quiz-question">客户账号手动打开 /app/risk/alerts,只靠前端能拦住吗?</h3>
|
||
<div class="quiz-options">
|
||
<button class="quiz-option" data-value="option-a" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>能,RequireAuth 够了</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-b" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>不能,必须后端 risk 角色 + X-Agent-Type: risk</span>
|
||
</button>
|
||
<button class="quiz-option" data-value="option-c" onclick="selectOption(this)">
|
||
<div class="quiz-option-radio"></div><span>能,Hash 路由自动 404</span>
|
||
</button>
|
||
</div>
|
||
<div class="quiz-feedback"></div>
|
||
</div>
|
||
<button class="quiz-check-btn" onclick="checkQuiz('quiz-frontend-m4')">检查答案</button>
|
||
<button class="quiz-reset-btn" onclick="resetQuiz('quiz-frontend-m4')">重做</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
</body>
|
||
</html>
|