Files
ephron-ren-prd/prd-prompt-csrf-and-blog-comment-empty-state.md

371 lines
12 KiB
Markdown
Raw Permalink 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.
# 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 = '<div class="no-comments"><div class="no-comments-icon">💬</div><p>暂无评论,来发表第一条评论吧!</p></div>';
return;
}
commentsList.innerHTML = comments.map(comment => renderComment(comment)).join('');
}
```
### 3.3 根因分析
这是一个纯展示层文案问题:
- 当前空态提示写死在前端模板 JS 中
- 文案过于主动,不符合当前页面的简洁展示要求
### 3.4 解决方案
有两种可选实现:
#### 方案 A保留空态容器但移除该句文案推荐
将空评论时的 HTML 改为:
- 不显示这句“暂无评论,来发表第一条评论吧!”
- 可以仅保留空容器
- 或仅保留图标
- 或改成空字符串
推荐最保守方案:
- 直接将 `commentsList.innerHTML = ''`
- 让评论区在无评论时不额外渲染提示文本
这样最符合“不要显示这句话”的字面要求,也最干净。
#### 方案 B替换为更中性的极简文案
例如仅显示:
- `暂无评论`
但由于需求明确说“不要显示这句话”,且没有明确要求保留任何替代文案,因此不建议自行引入新文案。
### 3.5 验收标准
1. 任意无评论博客详情页打开后
2. 评论区域不再显示:`暂无评论,来发表第一条评论吧!`
3. 不影响有评论时的正常渲染
4. 不影响评论数统计、评论提交、回复渲染等现有逻辑
---
## 四、实施范围
### Prompt 侧
- `prompt/src/routes/pages.py`
- `prompt/static/js/test-prompt.js`
- `prompt/tests/...`(补测试)
### Blog 侧
- `blog/templates/post.html`
---
## 五、风险与注意事项
### 5.1 Prompt CSRF 修复风险
- 若详情页每次 GET 都生成并覆盖 CSRF cookie需要确认不会破坏站内其他表单/接口的当前交互流程
- 但现有后台页已经是同样模式,因此风险较低
- 需要注意与 `shared/csrf.py` 的有效期和 secure/domain 配置保持一致
### 5.2 评论空态调整风险
- 若直接清空评论区,需要确认样式和布局不会出现异常留白
- 这是低风险前端调整,但建议实际看一眼无评论页面效果
---
## 六、上线后预期结果
### Prompt 详情页
用户首次直接进入公开提示词详情页测试时即可成功调用模型,不再依赖先访问 `/admin/settings` 页面“激活”环境。
### 博客详情页
无评论文章页将更干净,不再出现“暂无评论,来发表第一条评论吧!”的提示。
---
## 七、建议提交拆分
虽然本 PRD 合并记录了两个问题,但实际开发提交建议可按以下粒度:
1. `fix(prompt): initialize csrf cookie on public prompt detail page`
2. `fix(blog): remove no-comments prompt text on post detail page`
如果实现量很小,也可合并成一次小修提交,但 PR 描述中应明确包含两个修复点。