Skip to content

Conversation

smf-h
Copy link

@smf-h smf-h commented Oct 9, 2025

内容
/api/chat/send:新建/继续会话,内存记忆 + MySQL 持久化;历史窗口 2x max_history;无 ARK_API_KEY 时 mock
/api/chat/history:优先 Redis 最近缓存,未命中回源 DB 并回填缓存/内存
/api/chat/clear:支持 full;full=false 删除消息+缓存;full=true 额外删除会话元数据并清理内存;响应精简
/api/chat/list:返回会话 ID + last_active_at
/api/chat/debug/cache:DEBUG 时可查看最近缓存
权限/安全:JWT、用户等级与模型最小等级校验、会话归属校验
缓存/预热:chat:conv:recent:;/send 首轮 Redis→DB 预热内存
文档:同步更新 docs/api.md 等
迁移:messages.ConversationID 建议添加 ON DELETE CASCADE 外键(提供 SQL 示例

Summary by CodeRabbit

  • 新功能
    • 上线简化版 AI 多轮对话服务:登录/注册、JWT 鉴权、会话发送/历史/清空/列表
    • 支持模型权限控制与会话记忆;MySQL 持久化,Redis 近期历史缓存
    • 集成 Swagger 文档与可视化调试入口
  • 文档
    • 新增使用说明、API 参考、代码概览与安全配置指南
  • 杂务
    • 提供示例与默认配置文件,支持环境变量覆盖
    • 初始化项目依赖与模块配置

Copy link
Contributor

coderabbitai bot commented Oct 9, 2025

Walkthrough

新增一个完整的简化版多轮聊天服务:引入配置系统与示例配置、JWT 认证与中间件、内存会话管理、MySQL/GORM 模型与初始化、Redis 最近历史缓存、基于 Gin 的 API 路由与 Swagger 文档。实现注册/登录、聊天发送/历史/清理/会话列表与调试缓存的服务端逻辑。

Changes

Cohort / File(s) Summary
文档与概览
smf-h/README.md, smf-h/docs/api.md, smf-h/docs/code_overview.md, smf-h/docs/SECURITY_CONFIG.md
新增项目说明、接口文档、代码概览与安全配置指南,涵盖特性、目录、部署、错误码与扩展计划。
Swagger 规范与生成物
smf-h/docs/swagger/docs.go, smf-h/docs/swagger/swagger.yaml, smf-h/docs/swagger/swagger.json
引入 Swagger 元数据注册代码与 OpenAPI v2 规范(YAML/JSON),定义认证与聊天相关接口与模型;新增导出变量 SwaggerInfo 并在 init 中注册。
配置系统
smf-h/config/config.yaml, smf-h/config/config.example.yaml, smf-h/internal/config/config.go
加入实际与示例配置文件;实现 YAML 加载与环境变量覆盖(Server/MySQL/Chat/Auth/Redis/Models),公开配置结构体与加载函数。
认证与中间件
smf-h/internal/auth/auth.go, smf-h/internal/auth/middleware.go
新增 JWT 生成/解析、自定义 Claims、密码哈希校验;Gin 中间件校验 Bearer Token、加载用户并下发上下文,提供角色与等级读取辅助方法。
数据库与模型
smf-h/internal/db/mysql.go, smf-h/internal/db/tx.go, smf-h/internal/models/user.go, smf-h/internal/models/conversation.go, smf-h/internal/models/message.go
新增 GORM 初始化(连接池/日志级别)、占位 tx 文件;定义 User/Conversation/Message 模型与表名映射,包含索引/约束/字段类型。
会话内存管理
smf-h/internal/memory/conversation_memory.go
新增线程安全的会话内存与全局管理器,支持追加、裁剪(条数/Token)、最近 N 条、清空、列举会话 ID。
Redis 缓存
smf-h/internal/redisstore/redis.go
新增 Redis 客户端初始化与可用性探测;定义键前缀;实现最近消息 JSON 缓存的写入/读取/删除与会话相关键删除。
主服务与路由
smf-h/main.go
新增服务主入口:加载配置,初始化内存/模型权限、可选外部模型客户端、DB/Redis、自动迁移;注册健康、认证、聊天相关路由及统一错误结构;实现聊天发送/历史/清理/列表与调试缓存逻辑。
Go 模块
smf-h/go.mod
新增模块定义与依赖(gin、jwt、gorm、redis、swag 等)。

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant API(Gin)
  participant Auth(Middleware)
  participant DB(GORM)
  participant JWT
  participant Redis
  participant Mem(MemoryMgr)
  participant Model(SDK/Mock)

  rect rgb(245,248,255)
    note over Client,API: 注册/登录
    Client->>API: POST /api/auth/register
    API->>DB: 创建用户(哈希密码)
    DB-->>API: OK/冲突
    API-->>Client: 注册结果

    Client->>API: POST /api/auth/login
    API->>DB: 查询用户/校验密码
    API->>JWT: 生成 Token
    API-->>Client: token, user_id
  end

  rect rgb(245,255,245)
    note over Client,Model: 发送消息
    Client->>API: POST /api/chat/send {conversation_id, question, model}
    API->>Auth: 校验 Bearer
    Auth->>JWT: 解析 Token
    Auth->>DB: 读取用户信息
    Auth-->>API: 鉴权通过/失败
    API->>DB: 校验会话归属/存在或创建
    API->>Redis: 获取最近历史(JSON)
    alt 命中缓存
      Redis-->>API: 最近历史
      API->>Mem: 预热内存
    else 未命中
      API->>DB: 加载历史消息
      DB-->>API: 历史
      API->>Mem: 预热内存
    end
    API->>Model: 调用模型(或 Mock)
    Model-->>API: 答复
    API->>DB: 持久化问答消息
    API->>Redis: 缓存最近历史(JSON, TTL)
    API-->>Client: {answer, used_history, conversation_id}
  end
Loading
sequenceDiagram
  autonumber
  participant Client
  participant API
  participant Auth
  participant DB
  participant Redis
  participant Mem

  rect rgb(255,250,245)
    note over Client,API: 历史/清理/列表
    Client->>API: GET /api/chat/history?id=...
    API->>Auth: 校验
    API->>Redis: 取最近历史
    alt 命中
      Redis-->>API: 最近历史
    else 未命中
      API->>DB: 查询历史
      DB-->>API: 历史
      API->>Redis: 回填缓存
    end
    API-->>Client: {messages, count}

    Client->>API: POST /api/chat/clear {conversation_id}
    API->>Auth: 校验
    API->>DB: 删除会话消息/更新
    API->>Mem: 清空内存
    API->>Redis: 删除相关缓存键
    API-->>Client: 清理结果

    Client->>API: GET /api/chat/list
    API->>Auth: 校验
    API->>DB: 查询用户会话列表
    DB-->>API: 列表
    API-->>Client: {conversation_ids}
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

Poem

我是一只码场兔,耳朵竖成天线柱,
新服搭好风正足,JWT 随风发令符。
GORM 打拍 Redis 鼓,记忆滚动不迷途,
Swagger 展卷云上读,叮咚上线,跳个舞。 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed 该标题精准概括了本次 PR 的主要改动内容,涵盖了 AI 聊天服务新增的认证机制、会话持久化、Redis 缓存、列表和清理接口,以及文档同步更新,能够让团队快速了解核心新增功能。
Docstring Coverage ✅ Passed Docstring coverage is 90.63% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@smf-h
Copy link
Author

smf-h commented Oct 9, 2025

......

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🧹 Nitpick comments (10)
smf-h/README.md (1)

1-93: 建议改进 Markdown 格式以提升文档可读性。

静态分析工具标记了一些 Markdown 格式问题,虽然不影响功能,但修复后可提升文档的专业性和可读性:

  1. 在表格前后添加空行(第 12, 40, 57, 70 行)
  2. 为代码块指定语言标识符(第 24 行的环境变量示例可标记为 powershellbash

示例修复:

 
 ## 错误码对照(节选)
+
 | code | 含义 |
 |------|------|
-```
+```bash
 MYSQL_HOST / MYSQL_PORT / MYSQL_USER / MYSQL_PASSWORD / MYSQL_DB / MYSQL_CHARSET
 AUTH_JWT_SECRET / AUTH_ACCESS_TTL
smf-h/docs/api.md (1)

1-176: 建议改进 Markdown 格式。

文档内容详尽,但可改进格式以提升可读性:

  1. 在表格前后添加空行(第 27, 46, 86, 122, 136, 145, 158 行附近)
  2. 为代码块指定语言标识符,例如:
    • 第 42 行:json
    • 第 56 行:json
    • 第 122, 136, 145, 158 行:json 或其他适当的语言

示例:

-```
+```json
 {
   "conversation_id": "",
   "question": "你好",

根据编码指南(基于 learnings)。

smf-h/docs/SECURITY_CONFIG.md (3)

12-17: 修复 Markdown 表格格式问题。

根据 markdownlint 规则,表格前后应有空行以提升可读性。

在以下位置的表格前后添加空行:

  • Line 12(第2节表格)
  • Line 40(第5节表格)
  • Line 57(第7节表格)
  • Line 70(第9节表格)

示例修复:

 
+
 | 文件 | 作用 | 是否包含敏感信息 |
+

Also applies to: 40-45, 57-64, 70-75


24-31: 为代码块指定语言标识符。

Line 24 的代码块缺少语言标识符,建议添加以提升语法高亮和可读性。

应用此修改:

-```
+```bash
 SERVER_PORT, SERVER_MODE
 MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DB, MYSQL_CHARSET

83-83: 避免使用强调标记作为标题。

Line 83 使用了强调标记(**END**)而不是正式的标题标记。

建议替换为:

-**END**
+---
+文档结束
smf-h/docs/swagger/swagger.json (2)

572-578: 考虑为 API 文档添加 HTTPS 提示。

虽然 Bearer 令牌通过 Authorization 标头传递是标准做法,但应在生产环境中强制使用 HTTPS 以保护令牌传输。

建议在 API 文档的 info.description 或单独的安全章节中添加说明:

"description": "简化版 AI 问答系统接口,包含认证/聊天/会话与模型权限示例。⚠️ 生产环境必须使用 HTTPS。"

或在 schemes 中强制指定:

"schemes": ["https"]

364-369: 消息数组缺少分页机制。

ChatHistoryData.messages 数组没有最大数量限制或分页参数,可能导致大型会话的性能问题。

建议考虑以下选项之一:

  1. /api/chat/history 添加分页查询参数(limit, offset
  2. 在响应中添加 maxItems 约束并文档化默认限制
  3. ChatHistoryData 中添加 has_more 字段指示是否有更多历史

示例(添加分页参数):

{
  "name": "limit",
  "in": "query",
  "type": "integer",
  "default": 100,
  "description": "返回的最大消息数"
}
smf-h/docs/code_overview.md (1)

27-43: 修复多处 Markdown 格式问题。

文档存在以下格式问题:

  1. 表格前后缺少空行(Lines 46, 86, 244, 261, 271, 290)
  2. 代码块缺少语言标识符(Lines 27, 122, 136, 145, 155, 168, 179, 194, 228)

这些是 Markdown 最佳实践问题,建议批量修复以提升文档质量和工具兼容性。

示例修复:

 ---
 ## 2. 目录结构说明
-```
+```text
 .
 ├── main.go
 
+
 | 包 | 职责 | 依赖方向 |
+

Also applies to: 46-53, 86-91, 122-133, 136-142, 145-153, 155-158, 168-179, 244-250, 261-268, 271-279, 290-297

smf-h/internal/db/mysql.go (2)

29-31: 缺少 TLS/SSL 配置选项。

DSN 未包含 TLS 参数,生产环境中数据库连接应使用加密传输。

Config 结构体中添加 TLS 选项:

 type Config struct {
 	User     string
 	Password string
 	Host     string
 	Port     int
 	Database string
 	Charset  string
+	TLS      string // "true", "false", "skip-verify", or custom tls config name
 }

在 DSN 构建中使用:

 	dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local",
 		cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Database, cfg.Charset,
 	)
+	if cfg.TLS != "" {
+		dsn += "&tls=" + cfg.TLS
+	}

参考 smf-h/docs/SECURITY_CONFIG.md Line 60 中关于数据库 TLS 的建议。


33-36: 缺少连接超时和上下文支持。

gorm.Open 没有超时控制,可能在网络问题时无限期阻塞启动流程。

考虑添加连接超时:

-func Init(cfg Config) error {
+func Init(ctx context.Context, cfg Config) error {
+	// 添加超时控制
+	if _, hasDeadline := ctx.Deadline(); !hasDeadline {
+		var cancel context.CancelFunc
+		ctx, cancel = context.WithTimeout(ctx, 10*time.Second)
+		defer cancel()
+	}
+
 	if cfg.Charset == "" {
 		cfg.Charset = "utf8mb4"
 	}
 	dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local",
 		cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Database, cfg.Charset,
 	)
 	gormCfg := &gorm.Config{Logger: logger.Default.LogMode(logger.Warn)}
-	db, err := gorm.Open(mysql.Open(dsn), gormCfg)
+	db, err := gorm.Open(mysql.New(mysql.Config{
+		DSN: dsn,
+	}), gormCfg)

注意:调用方(main.go)需要相应更新为 db.Init(ctx, cfg)

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0275bb9 and f64c5be.

⛔ Files ignored due to path filters (1)
  • smf-h/go.sum is excluded by !**/*.sum
📒 Files selected for processing (21)
  • smf-h/README.md (1 hunks)
  • smf-h/config/config.example.yaml (1 hunks)
  • smf-h/config/config.yaml (1 hunks)
  • smf-h/docs/SECURITY_CONFIG.md (1 hunks)
  • smf-h/docs/api.md (1 hunks)
  • smf-h/docs/code_overview.md (1 hunks)
  • smf-h/docs/swagger/docs.go (1 hunks)
  • smf-h/docs/swagger/swagger.json (1 hunks)
  • smf-h/docs/swagger/swagger.yaml (1 hunks)
  • smf-h/go.mod (1 hunks)
  • smf-h/internal/auth/auth.go (1 hunks)
  • smf-h/internal/auth/middleware.go (1 hunks)
  • smf-h/internal/config/config.go (1 hunks)
  • smf-h/internal/db/mysql.go (1 hunks)
  • smf-h/internal/db/tx.go (1 hunks)
  • smf-h/internal/memory/conversation_memory.go (1 hunks)
  • smf-h/internal/models/conversation.go (1 hunks)
  • smf-h/internal/models/message.go (1 hunks)
  • smf-h/internal/models/user.go (1 hunks)
  • smf-h/internal/redisstore/redis.go (1 hunks)
  • smf-h/main.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
smf-h/internal/models/message.go (1)
smf-h/internal/memory/conversation_memory.go (1)
  • Message (14-18)
smf-h/docs/swagger/docs.go (1)
AmberJc1/controllers/chat.go (1)
  • Register (17-40)
smf-h/internal/auth/middleware.go (3)
smf-h/internal/auth/auth.go (1)
  • ParseToken (41-52)
smf-h/internal/models/user.go (2)
  • User (9-18)
  • User (20-20)
smf-h/internal/db/mysql.go (1)
  • Global (12-12)
smf-h/internal/redisstore/redis.go (2)
smf-h/internal/db/mysql.go (1)
  • Init (25-50)
smf-h/internal/config/config.go (1)
  • RedisConfig (49-58)
smf-h/internal/memory/conversation_memory.go (1)
smf-h/internal/models/message.go (2)
  • Message (6-15)
  • Message (17-17)
smf-h/internal/config/config.go (1)
smf-h/internal/models/user.go (2)
  • User (9-18)
  • User (20-20)
smf-h/internal/db/mysql.go (1)
smf-h/internal/models/user.go (2)
  • User (9-18)
  • User (20-20)
🪛 Checkov (3.2.334)
smf-h/docs/swagger/swagger.json

[high] 1-578: Ensure that the global security field has rules defined

(CKV_OPENAPI_4)


[high] 1-578: Ensure that security operations is not empty.

(CKV_OPENAPI_5)


[high] 572-578: Ensure that API keys are not sent over cleartext

(CKV_OPENAPI_20)


[medium] 364-369: Ensure that arrays have a maximum number of items

(CKV_OPENAPI_21)

smf-h/docs/swagger/swagger.yaml

[high] 1-386: Ensure that the global security field has rules defined

(CKV_OPENAPI_4)


[high] 1-386: Ensure that security operations is not empty.

(CKV_OPENAPI_5)


[medium] 56-60: Ensure that arrays have a maximum number of items

(CKV_OPENAPI_21)

🪛 markdownlint-cli2 (0.18.1)
smf-h/docs/api.md

27-27: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


46-46: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


86-86: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


122-122: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


136-136: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


145-145: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


158-158: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

smf-h/README.md

12-12: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


24-24: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


40-40: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


57-57: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


70-70: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


83-83: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

smf-h/docs/code_overview.md

27-27: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


46-46: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


86-86: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


122-122: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


136-136: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


145-145: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


158-158: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


179-179: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


194-194: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


228-228: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


244-244: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


261-261: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


271-271: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


290-290: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


300-300: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

smf-h/docs/SECURITY_CONFIG.md

12-12: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


24-24: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


40-40: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


57-57: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


70-70: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


83-83: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

🪛 OSV Scanner (2.2.3)
smf-h/go.mod

[HIGH] 1-1: github.com/golang-jwt/jwt/v5 5.2.1: Excessive memory allocation during header parsing in github.com/golang-jwt/jwt

(GO-2025-3553)


[HIGH] 1-1: github.com/golang-jwt/jwt/v5 5.2.1: jwt-go allows excessive memory allocation during header parsing

(GHSA-mh63-6h87-95cp)


[CRITICAL] 1-1: golang.org/x/crypto 0.25.0: Misuse of connection.serverAuthenticate may cause authorization bypass in golang.org/x/crypto

(GO-2024-3321)


[CRITICAL] 1-1: golang.org/x/crypto 0.25.0: Potential denial of service in golang.org/x/crypto

(GO-2025-3487)


[CRITICAL] 1-1: golang.org/x/crypto 0.25.0: golang.org/x/crypto Vulnerable to Denial of Service (DoS) via Slow or Incomplete Key Exchange

(GHSA-hcg3-q754-cr77)


[CRITICAL] 1-1: golang.org/x/crypto 0.25.0: Misuse of ServerConfig.PublicKeyCallback may cause authorization bypass in golang.org/x/crypto

(GHSA-v778-237x-gjrc)

🔇 Additional comments (18)
smf-h/internal/db/tx.go (1)

1-4: 占位符文件合理。

保留空文件并添加说明注释是避免编译错误的合理做法。当需要事务管理时再实现即可。

smf-h/config/config.example.yaml (1)

1-42: 示例配置文件设计良好。

此文件正确使用占位符(CHANGE_MECHANGE_ME_JWT_SECRET)来保护敏感信息,并提供了清晰的环境变量覆盖示例。这是配置管理的最佳实践。

smf-h/internal/models/user.go (1)

1-20: 用户模型设计良好。

模型设计亮点:

  • 第 13 行:Password 字段正确使用 json:"-" 排除,防止密码泄露
  • 注释详细说明了使用 varchar 而非 text 的原因(避免 MySQL utf8mb4 索引长度问题)
  • 字段类型、索引和默认值设置合理
smf-h/internal/models/conversation.go (1)

1-14: 会话模型设计合理。

模型结构清晰:

  • 使用 varchar(64) 作为主键 ID,适合存储 UUID
  • UserIDLastActiveAt 字段正确添加索引,支持高效的归属查询和排序
  • 注释明确说明会话仅存储元数据,消息在内存中管理
smf-h/docs/SECURITY_CONFIG.md (1)

1-83: 文档内容全面且结构清晰!

该安全配置指南涵盖了关键的安全实践:

  • 明确了不应提交的敏感信息
  • 提供了环境变量覆盖机制
  • 包含 PR 前检查清单和 CI/CD 建议
  • 涵盖了生产加固和应急响应

这对于防止密钥泄露和保护生产环境非常重要。

smf-h/docs/swagger/swagger.json (1)

1-579: Swagger 规范结构完整且定义清晰!

API 契约定义了:

  • 完整的认证流程(注册/登录)
  • 多轮对话接口(发送/历史/清空)
  • 详细的错误响应模型
  • Bearer 认证机制

结构化的错误码和响应格式有助于前端集成。

smf-h/docs/code_overview.md (3)

114-116: 关于历史窗口策略的说明值得强调。

文档提到使用 LastN(maxHistoryToUse * 2) 扩大窗口,这是一个重要的设计决策。

这种策略平衡了上下文连续性和性能,建议在代码注释中也同步说明这个 2x 倍率的设计意图。


127-127: Warm 机制描述准确。

文档清晰说明了会话预热流程:Redis recent → DB 全量回源。这与 PR 目标中的"Redis 缓存/列表与清理接口"一致。


1-300: 开发者文档内容详实且结构优秀!

该文档提供了:

  • 清晰的项目结构和职责划分
  • 详细的关键流程说明(发送/历史/清空)
  • 实用的扩展指南和测试建议
  • 完整的 FAQ 和故障排查

对新加入的开发者非常友好。

smf-h/internal/models/message.go (2)

11-11: Content 字段使用 mediumtext 是合理的选择。

mediumtext 支持最大 16MB 的文本,适合存储长对话内容。这比 text(64KB) 或 varchar 更合适。


12-12: TokenCount 字段与内存模型保持一致。

该字段与 smf-h/internal/memory/conversation_memory.go 中的 Message.TokenCount 对应,便于在持久化和内存表示之间转换。

smf-h/internal/auth/auth.go (3)

54-58: Bcrypt 密码哈希配置合理。

使用 bcrypt.DefaultCost(10) 是推荐的安全实践,在性能和安全性之间取得平衡。


26-38: JWT 生成逻辑正确。

GenerateToken 正确设置了 ExpiresAtIssuedAt 声明,符合 JWT 标准。


10-10: 在生产环境中务必覆盖 JWT 密钥
已验证:main.go(205行)通过 SetJWTSecret(cfg.Auth.JWTSecret) 使用配置中的 jwt_secret 覆盖默认值。请在生产环境的配置文件或环境变量中将 jwt_secret 设置为安全的非默认值,避免继续使用 "change_me_dev_secret"。参考 smf-h/docs/SECURITY_CONFIG.md

smf-h/docs/swagger/docs.go (3)

1-1: 已正确标记为自动生成的代码。

文件顶部的注释清楚地表明这是由 swaggo/swag 自动生成的,不应手动编辑。


588-600: SwaggerInfo 配置合理。

SwaggerInfo 的元数据配置(Version 0.4、BasePath "/"、Title 和 Description)与 PR 目标和其他 Swagger 文件一致。


1-604: 安装并运行 swag 重新生成
确保已安装 swaggo/swag,修改 smf-h/main.go 中的 Swagger 注释后运行 swag init --parseDependency --parseInternal -g smf-h/main.go -o smf-h/docs/swagger,文档已在 smf-h/docs/api.md:171smf-h/docs/code_overview.md:201 中记录。

smf-h/internal/db/mysql.go (1)

42-44: 连接池配置参数合理。

  • MaxIdleConns=10:保持 10 个空闲连接,减少连接建立开销
  • MaxOpenConns=50:最多 50 个并发连接,防止数据库过载
  • ConnMaxLifetime=1h:每小时回收连接,防止长连接问题

这些参数适合中小型应用。

Comment on lines +1 to +35
server:
port: 8080
mode: debug
mysql:
host: 127.0.0.1
port: 3306
user: root
password: 2823128231
database: ai_chat
charset: utf8mb4
chat:
default_model: doubao-seed-1-6-250615
max_history: 12
memory_limit_msgs: 200
request_timeout: 15s
auth:
jwt_secret: change_me_dev_secret
access_ttl: 24h
models:
- id: doubao-seed-1-6-250615
min_level: 1
- id: deepseek-v3-1-terminus
min_level: 3
- id: kimi-k2-250905
min_level: 5

redis:
host: 127.0.0.1
port: 6379
# password: 2823128231
db: 0
pool_size: 20
dial_timeout: 2s
read_timeout: 1s
write_timeout: 1s
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

严重安全问题:敏感配置文件不应提交到代码库。

此文件包含真实的敏感信息:

  • 第 8 行:MySQL 密码 2823128231
  • 第 17 行:JWT 密钥(虽然标记为开发用,但不应提交)
  • 第 30 行:Redis 密码(虽被注释,仍然暴露)

根据 PR 目标和 README.md(第 86 行)的明确说明:"不要提交 config/config.yaml"。

必须立即采取的措施:

  1. 从此 PR 中移除 config/config.yaml 文件
  2. 将其添加到 .gitignore(如果尚未添加)
  3. 使用 config.example.yaml 作为模板,用户应在本地复制并填入真实值
  4. 如果已经提交到历史记录,考虑使用 git filter-branch 或 BFG Repo-Cleaner 清理历史
  5. 立即轮换所有已暴露的密钥和密码

参考 config/config.example.yaml 的正确做法:使用 CHANGE_ME 等占位符,并通过环境变量覆盖敏感值。

🤖 Prompt for AI Agents
In smf-h/config/config.yaml lines 1-35: this file contains real secrets (MySQL
password, JWT secret, commented Redis password) and must not be in the repo;
remove config/config.yaml from the PR and working tree, add config/config.yaml
to .gitignore (or ensure config/ is ignored), replace it in the repo with a
config/config.example.yaml containing non-sensitive placeholders like CHANGE_ME
and documented env-variable overrides, instruct users to copy the example to
config/config.yaml locally, rotate the exposed credentials immediately, and if
secrets were already committed to history, run a history scrub (BFG or
git-filter-branch) to remove them from the repo history.

Comment on lines +101 to +164
"/api/chat/clear": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Chat"
],
"summary": "清空会话",
"parameters": [
{
"description": "会话ID",
"name": "body",
"in": "body",
"required": true,
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.ChatClearResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/main.BadRequestError"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/main.UnauthorizedError"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/main.ForbiddenConversationError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/main.InternalModelCallError"
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

/api/chat/clear 的请求体定义不够具体。

当前使用了通用的 additionalProperties: string 对象,缺少明确的字段定义。根据 PR 描述,该接口应支持 conversation_idfull 参数。

建议定义专用的请求体模型:

definitions 中添加:

"main.ChatClearRequest": {
  "type": "object",
  "required": ["conversation_id"],
  "properties": {
    "conversation_id": {
      "type": "string",
      "example": "c3a2f1c4-uuid"
    },
    "full": {
      "type": "boolean",
      "example": false,
      "description": "true=删除会话元数据,false=仅删除消息"
    }
  }
}

然后在 Line 125-129 引用:

"schema": {
  "$ref": "#/definitions/main.ChatClearRequest"
}
🤖 Prompt for AI Agents
In smf-h/docs/swagger/swagger.json around lines 101 to 164 the requestBody for
POST /api/chat/clear is defined as a generic object with
additionalProperties:string instead of a concrete model; add a new definition
main.ChatClearRequest in the definitions section with required conversation_id
(string) and optional full (boolean) and then replace the inline schema at the
/api/chat/clear parameters (lines ~125-129) to reference "$ref":
"#/definitions/main.ChatClearRequest" so the API documentation explicitly
documents conversation_id and full.

Comment on lines +1 to +384
basePath: /
definitions:
main.AuthData:
properties:
token:
example: eyJhbGciOiJIUzI1NiIs...
type: string
user_id:
example: 1
type: integer
type: object
main.AuthResponse:
properties:
code:
example: 0
type: integer
data:
$ref: '#/definitions/main.AuthData'
msg:
example: ok
type: string
type: object
main.BadRequestError:
properties:
code:
example: 40001
type: integer
msg:
example: '参数错误: question 必填'
type: string
type: object
main.ChatClearData:
properties:
conversation_id:
type: string
type: object
main.ChatClearResponse:
properties:
code:
example: 0
type: integer
data:
$ref: '#/definitions/main.ChatClearData'
msg:
example: ok
type: string
type: object
main.ChatHistoryData:
properties:
conversation_id:
type: string
count:
example: 4
type: integer
messages:
items:
$ref: '#/definitions/memory.Message'
type: array
type: object
main.ChatHistoryResponse:
properties:
code:
example: 0
type: integer
data:
$ref: '#/definitions/main.ChatHistoryData'
msg:
example: ok
type: string
type: object
main.ChatRequest:
properties:
conversation_id:
example: ""
type: string
model:
example: doubao-seed-1-6-250615
type: string
question:
example: 你好
type: string
type: object
main.ChatSendData:
properties:
answer:
example: 你好,我是AI助手
type: string
conversation_id:
example: c3a2f1c4-uuid
type: string
used_history:
example: 2
type: integer
type: object
main.ChatSendResponse:
properties:
code:
example: 0
type: integer
data:
$ref: '#/definitions/main.ChatSendData'
msg:
example: ok
type: string
type: object
main.ForbiddenConversationError:
properties:
code:
example: 40302
type: integer
msg:
example: 会话不属于当前用户
type: string
type: object
main.ForbiddenModelError:
properties:
code:
example: 40301
type: integer
msg:
example: 无权限使用模型(doubao-seed-1-6-250615),需要等级≥3
type: string
type: object
main.InternalEmptyReturnError:
properties:
code:
example: 50001
type: integer
msg:
example: 模型返回为空
type: string
type: object
main.InternalModelCallError:
properties:
code:
example: 50000
type: integer
msg:
example: 模型调用失败
type: string
type: object
main.ModelNotFoundError:
properties:
code:
example: 40002
type: integer
msg:
example: 模型不存在或未配置
type: string
type: object
main.UnauthorizedError:
properties:
code:
example: 40101
type: integer
msg:
example: 未提供或非法的 Authorization 头
type: string
type: object
main.loginReq:
properties:
email:
example: [email protected]
type: string
password:
example: Abcd1234!
type: string
username:
example: user1
type: string
required:
- password
type: object
main.registerReq:
properties:
email:
example: [email protected]
type: string
password:
example: Abcd1234!
type: string
username:
example: user1
type: string
required:
- password
- username
type: object
memory.Message:
properties:
content:
type: string
role:
type: string
token_count:
type: integer
type: object
info:
contact: {}
description: 简化版 AI 问答系统接口,包含认证/聊天/会话与模型权限示例。
title: AI Chat API
version: "0.4"
paths:
/api/auth/login:
post:
consumes:
- application/json
parameters:
- description: 登录参数 (username 或 email 任选其一)
in: body
name: body
required: true
schema:
$ref: '#/definitions/main.loginReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/main.AuthResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/main.BadRequestError'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/main.InternalModelCallError'
summary: 用户登录
tags:
- Auth
/api/auth/register:
post:
consumes:
- application/json
parameters:
- description: 注册参数
in: body
name: body
required: true
schema:
$ref: '#/definitions/main.registerReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/main.AuthResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/main.BadRequestError'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/main.InternalModelCallError'
summary: 用户注册
tags:
- Auth
/api/chat/clear:
post:
consumes:
- application/json
parameters:
- description: 会话ID
in: body
name: body
required: true
schema:
additionalProperties:
type: string
type: object
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/main.ChatClearResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/main.BadRequestError'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/main.UnauthorizedError'
"403":
description: Forbidden
schema:
$ref: '#/definitions/main.ForbiddenConversationError'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/main.InternalModelCallError'
security:
- BearerAuth: []
summary: 清空会话
tags:
- Chat
/api/chat/history:
get:
parameters:
- description: 会话ID
in: query
name: conversation_id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/main.ChatHistoryResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/main.BadRequestError'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/main.UnauthorizedError'
"403":
description: Forbidden
schema:
$ref: '#/definitions/main.ForbiddenConversationError'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/main.InternalModelCallError'
security:
- BearerAuth: []
summary: 获取历史
tags:
- Chat
/api/chat/send:
post:
consumes:
- application/json
description: 发送用户问题并返回模型回答,支持新建或继续会话
parameters:
- description: 请求体
in: body
name: body
required: true
schema:
$ref: '#/definitions/main.ChatRequest'
produces:
- application/json
responses:
"200":
description: 成功
schema:
$ref: '#/definitions/main.ChatSendResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/main.ModelNotFoundError'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/main.UnauthorizedError'
"403":
description: Forbidden
schema:
$ref: '#/definitions/main.ForbiddenConversationError'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/main.InternalEmptyReturnError'
security:
- BearerAuth: []
summary: 发送消息
tags:
- Chat
securityDefinitions:
BearerAuth:
in: header
name: Authorization
type: apiKey
swagger: "2.0"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

确保 YAML 和 JSON 规范保持同步。

swagger.yaml 和 swagger.json 是同一 API 规范的两种格式。确保使用 swag init 命令同步生成,避免手动编辑导致不一致。

运行以下命令验证两个文件是否保持同步:


🏁 Script executed:

#!/bin/bash
# 验证 swagger.json 和 swagger.yaml 内容是否一致

# 将 YAML 转为 JSON 并比较
yq eval swagger.yaml -o=json > /tmp/yaml_as_json.json
diff -u swagger.json /tmp/yaml_as_json.json || echo "文件不一致"

Length of output: 92


🏁 Script executed:

#!/bin/bash
# 查找 swagger.json 文件位置
fd swagger.json .

Length of output: 33


🏁 Script executed:

#!/bin/bash
set -e

# 安装 PyYAML
pip install pyyaml

# 使用 Python 将 YAML 转为 JSON
python3 - << 'EOF'
import yaml, json
with open('smf-h/docs/swagger/swagger.yaml', 'r', encoding='utf-8') as f:
    data = yaml.safe_load(f)
with open('/tmp/yaml_as_json.json', 'w', encoding='utf-8') as out:
    json.dump(data, out, ensure_ascii=False, sort_keys=True, indent=2)
EOF

# 比较生成的 JSON 与现有 swagger.json
diff -u smf-h/docs/swagger/swagger.json /tmp/yaml_as_json.json || echo "文件不一致"

Length of output: 32593


确保 swagger.yaml 与 swagger.json 保持同步
实际比对发现两者结构不一致,请在 smf-h/docs/swagger 目录使用 swag init 重新生成 swagger.json,避免手动编辑造成差异。

🧰 Tools
🪛 Checkov (3.2.334)

[high] 1-386: Ensure that the global security field has rules defined

(CKV_OPENAPI_4)


[high] 1-386: Ensure that security operations is not empty.

(CKV_OPENAPI_5)


[medium] 56-60: Ensure that arrays have a maximum number of items

(CKV_OPENAPI_21)

🤖 Prompt for AI Agents
smf-h/docs/swagger/swagger.yaml lines 1-384: the swagger.yaml and swagger.json
are out of sync; regenerate the JSON from the YAML to ensure both match. Run
`swag init` (or the project’s documented swagger generation command) in
smf-h/docs/swagger to produce an up-to-date swagger.json, replace the checked-in
swagger.json with the generated one (do not manually edit), run any
tests/linting that validate the spec, and commit the regenerated swagger.json so
both files remain synchronized.

Comment on lines +262 to +302
/api/chat/clear:
post:
consumes:
- application/json
parameters:
- description: 会话ID
in: body
name: body
required: true
schema:
additionalProperties:
type: string
type: object
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/main.ChatClearResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/main.BadRequestError'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/main.UnauthorizedError'
"403":
description: Forbidden
schema:
$ref: '#/definitions/main.ForbiddenConversationError'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/main.InternalModelCallError'
security:
- BearerAuth: []
summary: 清空会话
tags:
- Chat
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

与 swagger.json 一致:/api/chat/clear 请求体定义不够具体。

同 swagger.json 的问题,该接口缺少明确的 ChatClearRequest 定义,应包含 conversation_idfull 字段。

definitions 中添加:

main.ChatClearRequest:
  type: object
  required:
    - conversation_id
  properties:
    conversation_id:
      type: string
      example: c3a2f1c4-uuid
    full:
      type: boolean
      example: false
      description: true=删除会话元数据,false=仅删除消息

然后在 Line 271 引用:

schema:
  $ref: '#/definitions/main.ChatClearRequest'
🤖 Prompt for AI Agents
In smf-h/docs/swagger/swagger.yaml around lines 262 to 302, the POST
/api/chat/clear request body is defined as a generic object and must be replaced
with a concrete ChatClearRequest reference and a corresponding definition;
update the operation's parameters (around line 271) to set schema to $ref
'#/definitions/main.ChatClearRequest', and add a new definitions entry
main.ChatClearRequest that is an object requiring conversation_id and includes
properties conversation_id (string) and full (boolean) with examples and a brief
description for full (true=delete metadata, false=delete only messages).


require (
github.com/gin-gonic/gin v1.10.0
github.com/golang-jwt/jwt/v5 v5.2.1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

严重安全漏洞:升级 golang-jwt/jwt/v5 依赖。

当前版本 v5.2.1 存在 HIGH 级别安全漏洞:

  • GO-2025-3553 / GHSA-mh63-6h87-95cp:头部解析期间过度内存分配,可导致拒绝服务攻击

立即升级到最新版本:

-	github.com/golang-jwt/jwt/v5 v5.2.1
+	github.com/golang-jwt/jwt/v5 v5.2.2

运行 go get -u github.com/golang-jwt/jwt/v5@latest && go mod tidy 完成升级。

根据 learnings 中 jwt 库的最佳实践。

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/golang-jwt/jwt/v5 v5.2.2
🤖 Prompt for AI Agents
In smf-h/go.mod around line 9, the jwt dependency is pinned to v5.2.1 which has
a HIGH security vulnerability (GO-2025-3553); update the module to the latest
patched release by changing the version in go.mod to the latest (or run the
command below) and then tidy modules to update go.sum and vendor files: run `go
get -u github.com/golang-jwt/jwt/v5@latest && go mod tidy`, verify tests and CI
pass, and ensure any JWT usage follows the library best-practices from the
project's learnings.

Comment on lines +29 to +31
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local",
cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Database, cfg.Charset,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

DSN 中的密码可能在日志中泄露。

虽然 Line 46 的日志没有打印密码,但 GORM 的内部日志可能记录完整的 DSN 字符串(包含密码)。

建议:

  1. 确保 GORM 日志级别设置为 logger.Warn 或更高(Line 32 已设置,很好)
  2. 在生产环境中使用更严格的日志级别
  3. 考虑使用环境变量或密钥管理服务传递敏感配置

参考 smf-h/docs/SECURITY_CONFIG.md 中的最佳实践。

当前的 logger.Warn 级别是合理的,可以防止大部分敏感信息泄露。

🤖 Prompt for AI Agents
In smf-h/internal/db/mysql.go around lines 29 to 31, the DSN is constructed with
the plaintext password which risks exposure if any logger (GORM or otherwise)
captures the full string; keep GORM logger at logger.Warn or higher (as already
set), and update the code to avoid leaking secrets by (1) loading credentials
from environment/secret manager, (2) never logging the raw DSN — if you must log
connection info, build and log a redacted DSN that omits or replaces the
password, and (3) prefer using the driver/config structs or a connection config
API (that accepts credentials separately) so that any diagnostic logging can
omit sensitive fields; also ensure production logging level is stricter per
smf-h/docs/SECURITY_CONFIG.md.

// Message 持久化一条会话消息
type Message struct {
ID uint `gorm:"primaryKey" json:"id"`
ConversationID string `gorm:"type:varchar(64);index;not null;constraint:OnDelete:CASCADE,OnUpdate:CASCADE;" json:"conversation_id"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

外键约束语法可能不完整。

Line 8 的 constraint:OnDelete:CASCADE,OnUpdate:CASCADE 语法在 GORM 中应该使用 constraint 标签的 OnDeleteOnUpdate 选项,但缺少对 Conversation 表的显式外键声明。

GORM 的 constraint 标签需要配合外键关系使用。请验证:

  1. 是否在 Conversation 模型中定义了反向关联?
  2. 数据库是否正确创建了带 CASCADE 的外键约束?

建议的完整定义:

-ConversationID string    `gorm:"type:varchar(64);index;not null;constraint:OnDelete:CASCADE,OnUpdate:CASCADE;" json:"conversation_id"`
+ConversationID string    `gorm:"type:varchar(64);index;not null" json:"conversation_id"`

然后在 Conversation 模型中添加:

// in conversation.go
type Conversation struct {
    ID       string    `gorm:"type:varchar(64);primaryKey"`
    Messages []Message `gorm:"foreignKey:ConversationID;constraint:OnDelete:CASCADE,OnUpdate:CASCADE"`
    // ... other fields
}

运行以下脚本验证实际生成的表结构:


🏁 Script executed:

#!/bin/bash
# 检查 Conversation 模型是否定义了反向关联
rg -n "type Conversation struct" -A 10 smf-h/internal/models/

# 检查是否在其他地方定义了外键关系
rg -n "foreignKey.*ConversationID" smf-h/internal/models/

Length of output: 743


🏁 Script executed:

#!/bin/bash
rg -n "type Message struct" -A 20 smf-h/internal/models/message.go
rg -n "Conversation" smf-h/internal/models/message.go

Length of output: 855


为 Conversation 模型添加反向外键关联

  • 当前 conversation.go 中缺少 Messages []Message 字段,无法让 GORM 根据 ConversationID 自动创建带 CASCADE 的外键约束
    建议在 Conversation struct 中添加:
Messages []Message `gorm:"foreignKey:ConversationID;constraint:OnDelete:CASCADE,OnUpdate:CASCADE"`  
🤖 Prompt for AI Agents
In smf-h/internal/models/message.go around line 8 the ConversationID field on
Message has CASCADE constraints but the Conversation model lacks a reverse
association, so GORM cannot automatically create the bidirectional FK with
cascade; update the Conversation struct (in conversation.go) to add a Messages
[]Message field with tag foreignKey:ConversationID and
constraint:OnDelete:CASCADE,OnUpdate:CASCADE so GORM will generate the reverse
relation and enforce cascade behavior.

Comment on lines +288 to +295
if req.ConversationID == "" {
req.ConversationID = uuid.NewString()
// 创建会话记录并归属当前用户
uidVal, _ := c.Get(auth.CtxUserIDKey)
uid, _ := uidVal.(uint)
conv := models.Conversation{ID: req.ConversationID, UserID: uid, LastActiveAt: time.Now()}
_ = db.Global.Create(&conv).Error
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

不要忽略创建会话失败的错误

当新建会话(Line 288-295)时,如果 db.Global.Create(&conv) 返回错误(例如数据库不可达或唯一约束冲突),当前实现直接吞掉错误并继续后续流程,最终导致整段对话都无法持久化但接口仍返回成功。这会让用户拿到一个看似成功的响应,实际数据却丢失。请在失败时立即返回 500,并停止后续处理。

-		_ = db.Global.Create(&conv).Error
+		if err := db.Global.Create(&conv).Error; err != nil {
+			writeErr(c, http.StatusInternalServerError, CodeInternalError, "会话创建失败")
+			return
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if req.ConversationID == "" {
req.ConversationID = uuid.NewString()
// 创建会话记录并归属当前用户
uidVal, _ := c.Get(auth.CtxUserIDKey)
uid, _ := uidVal.(uint)
conv := models.Conversation{ID: req.ConversationID, UserID: uid, LastActiveAt: time.Now()}
_ = db.Global.Create(&conv).Error
}
if req.ConversationID == "" {
req.ConversationID = uuid.NewString()
// 创建会话记录并归属当前用户
uidVal, _ := c.Get(auth.CtxUserIDKey)
uid, _ := uidVal.(uint)
conv := models.Conversation{ID: req.ConversationID, UserID: uid, LastActiveAt: time.Now()}
if err := db.Global.Create(&conv).Error; err != nil {
writeErr(c, http.StatusInternalServerError, CodeInternalError, "会话创建失败")
return
}
}
🤖 Prompt for AI Agents
In smf-h/main.go around lines 288 to 295, the code ignores the error returned by
db.Global.Create(&conv); change this to capture the returned error, and if
non-nil respond immediately with HTTP 500 (and an error message) and stop
further processing (don't continue creating/using the conversation). Ensure you
log the DB error for internal diagnostics and return a proper JSON error
response to the client so the request does not appear successful when the
conversation was not persisted.

Comment on lines +300 to +313
// 会话归属验证(如果会话存在则检查 user_id)
if req.ConversationID != "" {
uidVal, _ := c.Get(auth.CtxUserIDKey)
uid, _ := uidVal.(uint)
var conv models.Conversation
if err := db.Global.First(&conv, "id = ?", req.ConversationID).Error; err == nil {
if conv.UserID != uid {
writeErr(c, http.StatusForbidden, CodeForbiddenConversation, "会话不属于当前用户")
return
}
// 更新活跃时间
_ = db.Global.Model(&conv).Update("last_active_at", time.Now()).Error
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

会话不存在时应及时返回错误

目前如果 db.Global.First 找不到会话(Line 305-313),代码会悄悄继续执行,后续写消息时由于 models.Messageinternal/models/message.go 中声明了外键约束,会直接写库失败并被忽略,导致整个会话既不存在又无法正确保存。建议区分“未找到”与“查询失败”,对前者返回 404/403,对后者返回 500,并在文件顶部补充 errorsgorm.io/gorm 的 import。

-		if err := db.Global.First(&conv, "id = ?", req.ConversationID).Error; err == nil {
-			if conv.UserID != uid {
-				writeErr(c, http.StatusForbidden, CodeForbiddenConversation, "会话不属于当前用户")
-				return
-			}
-			// 更新活跃时间
-			_ = db.Global.Model(&conv).Update("last_active_at", time.Now()).Error
-		}
+		if err := db.Global.First(&conv, "id = ?", req.ConversationID).Error; err != nil {
+			if errors.Is(err, gorm.ErrRecordNotFound) {
+				writeErr(c, http.StatusBadRequest, CodeBadParam, "会话不存在")
+			} else {
+				writeErr(c, http.StatusInternalServerError, CodeInternalError, "会话查询失败")
+			}
+			return
+		}
+		if conv.UserID != uid {
+			writeErr(c, http.StatusForbidden, CodeForbiddenConversation, "会话不属于当前用户")
+			return
+		}
+		if err := db.Global.Model(&conv).Update("last_active_at", time.Now()).Error; err != nil {
+			writeErr(c, http.StatusInternalServerError, CodeInternalError, "会话更新时间失败")
+			return
+		}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In smf-h/main.go around lines 300 to 313, when db.Global.First cannot find the
conversation the code currently silently continues; distinguish NotFound from
other DB errors and return appropriate HTTP responses: if
gorm.IsRecordNotFoundError (or errors.Is(err, gorm.ErrRecordNotFound)) return
404 (or 403 per policy) with a clear message, for other errors return 500, and
stop further processing; also ensure you import "errors" and "gorm.io/gorm" at
the file top so you can use errors.Is and gorm.ErrRecordNotFound.

Comment on lines +333 to +372
userTokenCount := roughTokenEstimate(req.Question)
cm.Append(memory.Message{Role: "user", Content: req.Question, TokenCount: userTokenCount})
// 持久化用户消息
uidVal, _ := c.Get(auth.CtxUserIDKey)
uid, _ := uidVal.(uint)
_ = db.Global.Create(&models.Message{ConversationID: req.ConversationID, UserID: uid, Role: "user", Content: req.Question, TokenCount: userTokenCount}).Error

// 准备历史:之前只取 maxHistoryToUse,容易导致上下文太短;
// 为了让模型看到更多往返,这里扩大到 *2(与缓存策略一致)。
contextWindow := maxHistoryToUse * 2
if contextWindow <= 0 {
contextWindow = maxHistoryToUse
}
history := cm.LastN(contextWindow)
modelMsgs := convertToModelMessages(history)

var answer string
if client == nil { // mock 模式
answer = fmt.Sprintf("[mock:%s] 你说: %s", req.Model, req.Question)
} else {
resp, err := client.CreateChatCompletion(c.Request.Context(), model.CreateChatCompletionRequest{
Model: req.Model,
Messages: modelMsgs,
})
if err != nil {
writeErr(c, http.StatusInternalServerError, CodeInternalError, "模型调用失败")
return
}
// 根据 SDK 结构:如果 Message 不是指针则无需判空;只校验 Choices 与 Content 指针
if len(resp.Choices) == 0 || resp.Choices[0].Message.Content == nil || resp.Choices[0].Message.Content.StringValue == nil {
writeErr(c, http.StatusInternalServerError, CodeModelEmpty, "模型返回为空")
return
}
answer = *resp.Choices[0].Message.Content.StringValue
}
ansToken := roughTokenEstimate(answer)
cm.Append(memory.Message{Role: "assistant", Content: answer, TokenCount: ansToken})
// 持久化助手回复
_ = db.Global.Create(&models.Message{ConversationID: req.ConversationID, UserID: uid, Role: "assistant", Content: answer, TokenCount: ansToken}).Error

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

持久化消息失败时必须立即终止

无论是写入用户消息还是助手回复(Line 336-372),现在都把 db.Global.Create 的错误直接丢弃。数据库只要出任何问题(连接、外键、约束等),接口仍会返回成功,但历史记录全部缺失,后续 /history 和内存预热都会得到空结果。这是核心数据路径,必须在失败时返回错误并终止处理。

-	_ = db.Global.Create(&models.Message{ConversationID: req.ConversationID, UserID: uid, Role: "user", Content: req.Question, TokenCount: userTokenCount}).Error
+	if err := db.Global.Create(&models.Message{
+		ConversationID: req.ConversationID,
+		UserID:         uid,
+		Role:           "user",
+		Content:        req.Question,
+		TokenCount:     userTokenCount,
+	}).Error; err != nil {
+		writeErr(c, http.StatusInternalServerError, CodeInternalError, "消息持久化失败")
+		return
+	}
@@
-	_ = db.Global.Create(&models.Message{ConversationID: req.ConversationID, UserID: uid, Role: "assistant", Content: answer, TokenCount: ansToken}).Error
+	if err := db.Global.Create(&models.Message{
+		ConversationID: req.ConversationID,
+		UserID:         uid,
+		Role:           "assistant",
+		Content:        answer,
+		TokenCount:     ansToken,
+	}).Error; err != nil {
+		writeErr(c, http.StatusInternalServerError, CodeInternalError, "助手消息持久化失败")
+		return
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
userTokenCount := roughTokenEstimate(req.Question)
cm.Append(memory.Message{Role: "user", Content: req.Question, TokenCount: userTokenCount})
// 持久化用户消息
uidVal, _ := c.Get(auth.CtxUserIDKey)
uid, _ := uidVal.(uint)
_ = db.Global.Create(&models.Message{ConversationID: req.ConversationID, UserID: uid, Role: "user", Content: req.Question, TokenCount: userTokenCount}).Error
// 准备历史:之前只取 maxHistoryToUse,容易导致上下文太短;
// 为了让模型看到更多往返,这里扩大到 *2(与缓存策略一致)。
contextWindow := maxHistoryToUse * 2
if contextWindow <= 0 {
contextWindow = maxHistoryToUse
}
history := cm.LastN(contextWindow)
modelMsgs := convertToModelMessages(history)
var answer string
if client == nil { // mock 模式
answer = fmt.Sprintf("[mock:%s] 你说: %s", req.Model, req.Question)
} else {
resp, err := client.CreateChatCompletion(c.Request.Context(), model.CreateChatCompletionRequest{
Model: req.Model,
Messages: modelMsgs,
})
if err != nil {
writeErr(c, http.StatusInternalServerError, CodeInternalError, "模型调用失败")
return
}
// 根据 SDK 结构:如果 Message 不是指针则无需判空;只校验 Choices 与 Content 指针
if len(resp.Choices) == 0 || resp.Choices[0].Message.Content == nil || resp.Choices[0].Message.Content.StringValue == nil {
writeErr(c, http.StatusInternalServerError, CodeModelEmpty, "模型返回为空")
return
}
answer = *resp.Choices[0].Message.Content.StringValue
}
ansToken := roughTokenEstimate(answer)
cm.Append(memory.Message{Role: "assistant", Content: answer, TokenCount: ansToken})
// 持久化助手回复
_ = db.Global.Create(&models.Message{ConversationID: req.ConversationID, UserID: uid, Role: "assistant", Content: answer, TokenCount: ansToken}).Error
userTokenCount := roughTokenEstimate(req.Question)
cm.Append(memory.Message{Role: "user", Content: req.Question, TokenCount: userTokenCount})
// 持久化用户消息
uidVal, _ := c.Get(auth.CtxUserIDKey)
uid, _ := uidVal.(uint)
if err := db.Global.Create(&models.Message{
ConversationID: req.ConversationID,
UserID: uid,
Role: "user",
Content: req.Question,
TokenCount: userTokenCount,
}).Error; err != nil {
writeErr(c, http.StatusInternalServerError, CodeInternalError, "消息持久化失败")
return
}
// 准备历史:之前只取 maxHistoryToUse,容易导致上下文太短;
// 为了让模型看到更多往返,这里扩大到 *2(与缓存策略一致)。
contextWindow := maxHistoryToUse * 2
if contextWindow <= 0 {
contextWindow = maxHistoryToUse
}
history := cm.LastN(contextWindow)
modelMsgs := convertToModelMessages(history)
var answer string
if client == nil { // mock 模式
answer = fmt.Sprintf("[mock:%s] 你说: %s", req.Model, req.Question)
} else {
resp, err := client.CreateChatCompletion(c.Request.Context(), model.CreateChatCompletionRequest{
Model: req.Model,
Messages: modelMsgs,
})
if err != nil {
writeErr(c, http.StatusInternalServerError, CodeInternalError, "模型调用失败")
return
}
// 根据 SDK 结构:如果 Message 不是指针则无需判空;只校验 Choices 与 Content 指针
if len(resp.Choices) == 0 || resp.Choices[0].Message.Content == nil || resp.Choices[0].Message.Content.StringValue == nil {
writeErr(c, http.StatusInternalServerError, CodeModelEmpty, "模型返回为空")
return
}
answer = *resp.Choices[0].Message.Content.StringValue
}
ansToken := roughTokenEstimate(answer)
cm.Append(memory.Message{Role: "assistant", Content: answer, TokenCount: ansToken})
// 持久化助手回复
if err := db.Global.Create(&models.Message{
ConversationID: req.ConversationID,
UserID: uid,
Role: "assistant",
Content: answer,
TokenCount: ansToken,
}).Error; err != nil {
writeErr(c, http.StatusInternalServerError, CodeInternalError, "助手消息持久化失败")
return
}
🤖 Prompt for AI Agents
In smf-h/main.go around lines 333 to 372, the two calls to db.Global.Create that
persist the user message and the assistant reply currently ignore returned
errors; detect and handle errors from both Create calls by capturing the
returned error, logging it, sending an HTTP error response (e.g., writeErr with
http.StatusInternalServerError and an appropriate code/message), and returning
immediately to stop further processing; ensure you do this for the user-message
persistence right after creating the Message for the user and again for the
assistant-message persistence after appending the assistant message.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant