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

12 KiB
Raw Permalink Blame History

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

@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

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

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

@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.pyprompt_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() 前增加判断:

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”的集成缺口。

建议新增测试:

文件建议: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} 在评论列表为空时,会显示带图标的空态提示:

💬
暂无评论,来发表第一条评论吧!

需求是:

博客详情页不要显示这句话。

3.2 已定位的源码

文件:blog/templates/post.html

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 描述中应明确包含两个修复点。