diff --git a/prd-prompt-csrf-and-blog-comment-empty-state.md b/prd-prompt-csrf-and-blog-comment-empty-state.md new file mode 100644 index 0000000..14c448f --- /dev/null +++ b/prd-prompt-csrf-and-blog-comment-empty-state.md @@ -0,0 +1,370 @@ +# Prompt 详情页测试 CSRF 初始化缺失 + 博客详情页评论空态文案优化 + +> **版本**: v1.0 +> **日期**: 2026-05-24 +> **状态**: 📝 待评审 + +--- + +## 一、背景与目标 + +本次 PRD 合并处理两个独立但都较明确的问题: + +1. **Prompt 详情页测试功能首次使用时报 CSRF token 验证失败** + 用户在已登录状态下访问公开提示词详情页(如 `/prompts/{key}`),在测试面板中选择模型、填写 user 输入后点击“开始测试”,首次请求可能直接返回 `CSRF token 验证失败`。但如果先进入 `/admin/settings` 页面并执行一次“测试模型连通性”,再回到相同提示词详情页,使用完全相同输入重新点击“开始测试”,则请求又可以正常成功。 + +2. **博客详情页无评论时的空态文案不符合期望** + 当前 `/posts/{slug}` 页面在无评论时会显示: + `💬 暂无评论,来发表第一条评论吧!` + 需求是:**博客详情页不要显示这句话**。 + +这两个问题都属于前端展示/交互层的可预期缺陷,适合在同一轮小修中一起纳入。 + +--- + +## 二、问题一:Prompt 详情页首次测试触发 CSRF 校验失败 + +### 2.1 现象 + +以页面: +`https://prompt.ephron.ren/prompts/prompt-20260508110413` +为例,在已登录状态下: + +1. 打开提示词详情页 +2. 切换到“测试”标签 +3. 正确选择模型 +4. 输入 user 内容 +5. 点击“开始测试” + +**实际行为**:返回 `CSRF token 验证失败` + +随后: + +1. 访问 `https://prompt.ephron.ren/admin/settings` +2. 点击测试模型连通性 +3. 页面提示 `连接成功!模型: deepseek-v4-flash` +4. 再返回刚才的提示词详情页 +5. 保持完全相同的模型与输入,再次点击“开始测试” + +**实际行为**:此时请求成功,模型可以正常调用。 + +### 2.2 影响范围 + +- 影响所有带“测试”面板的公开提示词详情页:`/prompts/{key}` +- 影响所有已登录但**首次从公开详情页直接发起测试**的用户 +- 若用户此前访问过某些会设置 CSRF cookie 的后台页面,问题可能被“掩盖” +- 不影响仅浏览正文、不使用测试功能的用户 + +### 2.3 已定位的源码 + +#### 详情页路由 +文件:`prompt/src/routes/pages.py` + +```python +@router.get("/prompts/{key}", response_class=HTMLResponse) +async def prompt_detail(request: Request, key: str): + prompt = get_prompt(key) + ... + return templates.TemplateResponse( + "public/detail.html", + { + "request": request, + "prompt": prompt, + "split_csv": _split_csv, + "models": models, + }, + ) +``` + +当前实现直接渲染详情页,但**没有为该页面生成并下发 CSRF cookie**。 + +#### 详情页测试前端 +文件:`prompt/static/js/test-prompt.js` + +```javascript +const csrfToken = document.cookie.match(/ephron_csrf=([^;]+)/)?.[1] || ''; +const response = await fetch(`/api/prompts/${this.promptKey}/test`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": csrfToken, + }, + body: JSON.stringify(body), + signal: this.abortController.signal, +}); +``` + +前端完全依赖 `document.cookie` 中存在 `ephron_csrf`。如果 cookie 不存在,就会发送空字符串。 + +#### 测试接口后端 +文件:`prompt/src/routes/api.py` + +```python +csrf_cookie = request.cookies.get("ephron_csrf") +csrf_header = request.headers.get("X-CSRF-Token") +if not verify_csrf_token(csrf_header, csrf_cookie): + raise HTTPException(status_code=403, detail="CSRF token 验证失败") +``` + +后端要求: +- cookie 中有 `ephron_csrf` +- header 中有 `X-CSRF-Token` +- 两者一致 + +否则直接返回 403。 + +#### admin/settings 页面为何会“治好”问题 +文件:`prompt/src/routes/admin.py` + +```python +@router.get("/settings", response_class=HTMLResponse) +async def settings_page(...): + ... + csrf_token = generate_csrf_token() + response = templates.TemplateResponse(...) + set_csrf_cookie(response, csrf_token, is_development=IS_DEVELOPMENT) + return response +``` + +即:访问 `/admin/settings` 时,服务端会显式生成 token 并设置 `ephron_csrf` cookie。 +因此用户访问过后台设置页后,详情页测试 JS 才终于能从 `document.cookie` 里读到 token,于是测试恢复正常。 + +### 2.4 根因分析 + +根因不是模型配置问题,也不是测试接口本身偶发失败,而是: + +> **公开提示词详情页的测试功能依赖 CSRF cookie,但详情页自身没有负责初始化这个 cookie。** + +换言之: + +- **依赖方**:`/prompts/{key}` 页面的“开始测试”功能 +- **提供方**:当前却变成了 `/admin/settings` 这类后台管理页面 +- **结果**:首次从公开详情页直接测试时,前置条件不满足,导致必现/高概率出现 403 + +这是一个**初始化时机错误**问题: +- 测试功能属于公开详情页的一部分 +- 因此其所依赖的 CSRF token 也应由详情页自身在 GET 阶段准备好 +- 不应要求用户先访问后台页面“预热”环境 + +### 2.5 解决方案 + +#### 方案 A:在公开详情页 GET 路由中补发 CSRF cookie(推荐) + +在 `prompt/src/routes/pages.py` 的 `prompt_detail()` 中: + +1. 生成新的 CSRF token +2. 用 `TemplateResponse` 组装响应对象 +3. 调用共享的 `set_csrf_cookie(response, csrf_token, is_development=IS_DEVELOPMENT)` +4. 返回 response + +参考后台页面已有模式,保持站点内 CSRF 初始化策略一致。 + +**优点**: +- 根因级修复 +- 用户首次进入详情页即可直接测试 +- 行为与后台页面一致,代码路径清晰 +- 不需要新增 API,不引入额外异步初始化流程 + +#### 方案 B:前端在缺 token 时给出明确提示(推荐作为兜底,而不是主修复) + +在 `prompt/static/js/test-prompt.js` 中: + +- 若 `document.cookie` 中取不到 `ephron_csrf` +- 不要直接发请求 +- 改为在界面中提示: + - `安全令牌未初始化,请刷新页面后重试` + - 或更温和的错误提示文案 + +**说明**:这只能改善体验,不能替代 A。因为没有根修复时,刷新也不一定能拿到 token;只有详情页本身负责种 cookie,刷新才有意义。 + +### 2.6 实现建议 + +#### 后端改动 +文件:`prompt/src/routes/pages.py` + +- 增加对共享 CSRF 工具的导入: + - `generate_csrf_token` + - `set_csrf_cookie` +- 在 `prompt_detail()` 中: + - 先生成 token + - 再构造 `TemplateResponse` + - 再对 response 调用 `set_csrf_cookie()` + +实现形态建议与 `admin.py` 保持一致,避免两套风格。 + +#### 前端改动 +文件:`prompt/static/js/test-prompt.js` + +在执行 `fetch()` 前增加判断: + +```javascript +const csrfToken = document.cookie.match(/ephron_csrf=([^;]+)/)?.[1] || ''; +if (!csrfToken) { + throw new Error('安全令牌未初始化,请刷新页面后重试'); +} +``` + +这样至少避免把“空 token”静默发到后端。 + +### 2.7 验收标准 + +#### 功能验收 +1. 已登录用户首次直接访问任意公开提示词详情页 +2. 不访问任何后台页面 +3. 直接进入“测试”标签 +4. 选择模型、输入 user 内容后点击“开始测试” +5. 请求成功,页面正常流式返回模型输出 + +#### 回归验收 +1. 后台 `admin/settings` 的连通性测试功能不受影响 +2. 详情页在未登录状态下仍保持原有鉴权行为(如测试接口要求登录则仍返回 401) +3. 详情页正文、标签、示例、模型下拉等渲染不受影响 + +#### 失败场景验收 +1. 若前端在极端情况下仍未读取到 token,应展示可理解错误提示,而不是只有通用“请求失败” +2. 不应再出现“访问后台设置页后才恢复”的异常依赖关系 + +### 2.8 测试补齐建议 + +现有测试中,很多 API case 是直接手动塞入: +- `ephron_auth` +- `ephron_csrf` +- `X-CSRF-Token` + +这验证了 API 校验本身,但掩盖了“公开详情页没有负责初始化 cookie”的集成缺口。 + +建议新增测试: + +#### 集成测试 1:详情页 GET 应设置 CSRF cookie +文件建议:`prompt/tests/test_prompt_service_api.py` 或更适合的 pages 路由测试文件 + +步骤: +1. 创建一个可访问的 active prompt +2. 使用已登录 client 访问 `/prompts/{key}` +3. 断言响应或 client cookies 中出现 `ephron_csrf` + +#### 集成测试 2:首次访问详情页后可直接调用测试接口 +步骤: +1. 已登录 client 先 GET `/prompts/{key}` +2. 从 client cookies 中读取 `ephron_csrf` +3. 直接 POST `/api/prompts/{key}/test` +4. header 中传同一 token +5. 断言不返回 403 CSRF 错误 + +--- + +## 三、问题二:博客详情页无评论时不要显示“暂无评论,来发表第一条评论吧!” + +### 3.1 现象 + +当前博客详情页 `/posts/{slug}` 在评论列表为空时,会显示带图标的空态提示: + +```text +💬 +暂无评论,来发表第一条评论吧! +``` + +需求是: + +> **博客详情页不要显示这句话。** + +### 3.2 已定位的源码 + +文件:`blog/templates/post.html` + +```javascript +function renderComments(comments) { + if (comments.length === 0) { + commentsList.innerHTML = '
暂无评论,来发表第一条评论吧!