-
Notifications
You must be signed in to change notification settings - Fork 19
AI Chat Service: 认证/会话持久化/Redis 缓存/列表与清理接口 + 文档同步 #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…t/clear endpoints, docs
Walkthrough新增一个完整的简化版多轮聊天服务:引入配置系统与示例配置、JWT 认证与中间件、内存会话管理、MySQL/GORM 模型与初始化、Redis 最近历史缓存、基于 Gin 的 API 路由与 Swagger 文档。实现注册/登录、聊天发送/历史/清理/会话列表与调试缓存的服务端逻辑。 Changes
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
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120+ minutes Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
...... |
There was a problem hiding this 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 格式问题,虽然不影响功能,但修复后可提升文档的专业性和可读性:
- 在表格前后添加空行(第 12, 40, 57, 70 行)
- 为代码块指定语言标识符(第 24 行的环境变量示例可标记为
powershell
或bash
)示例修复:
## 错误码对照(节选) + | code | 含义 | |------|------|
-``` +```bash MYSQL_HOST / MYSQL_PORT / MYSQL_USER / MYSQL_PASSWORD / MYSQL_DB / MYSQL_CHARSET AUTH_JWT_SECRET / AUTH_ACCESS_TTLsmf-h/docs/api.md (1)
1-176
: 建议改进 Markdown 格式。文档内容详尽,但可改进格式以提升可读性:
- 在表格前后添加空行(第 27, 46, 86, 122, 136, 145, 158 行附近)
- 为代码块指定语言标识符,例如:
- 第 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
数组没有最大数量限制或分页参数,可能导致大型会话的性能问题。建议考虑以下选项之一:
- 在
/api/chat/history
添加分页查询参数(limit
,offset
)- 在响应中添加
maxItems
约束并文档化默认限制- 在
ChatHistoryData
中添加has_more
字段指示是否有更多历史示例(添加分页参数):
{ "name": "limit", "in": "query", "type": "integer", "default": 100, "description": "返回的最大消息数" }smf-h/docs/code_overview.md (1)
27-43
: 修复多处 Markdown 格式问题。文档存在以下格式问题:
- 表格前后缺少空行(Lines 46, 86, 244, 261, 271, 290)
- 代码块缺少语言标识符(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
⛔ 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
[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
[CRITICAL] 1-1: golang.org/x/crypto 0.25.0: Misuse of ServerConfig.PublicKeyCallback may cause authorization bypass in golang.org/x/crypto
🔇 Additional comments (18)
smf-h/internal/db/tx.go (1)
1-4
: 占位符文件合理。保留空文件并添加说明注释是避免编译错误的合理做法。当需要事务管理时再实现即可。
smf-h/config/config.example.yaml (1)
1-42
: 示例配置文件设计良好。此文件正确使用占位符(
CHANGE_ME
、CHANGE_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,适合存储 UUIDUserID
和LastActiveAt
字段正确添加索引,支持高效的归属查询和排序- 注释明确说明会话仅存储元数据,消息在内存中管理
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
正确设置了ExpiresAt
和IssuedAt
声明,符合 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:171
与smf-h/docs/code_overview.md:201
中记录。smf-h/internal/db/mysql.go (1)
42-44
: 连接池配置参数合理。
MaxIdleConns=10
:保持 10 个空闲连接,减少连接建立开销MaxOpenConns=50
:最多 50 个并发连接,防止数据库过载ConnMaxLifetime=1h
:每小时回收连接,防止长连接问题这些参数适合中小型应用。
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
严重安全问题:敏感配置文件不应提交到代码库。
此文件包含真实的敏感信息:
- 第 8 行:MySQL 密码
2823128231
- 第 17 行:JWT 密钥(虽然标记为开发用,但不应提交)
- 第 30 行:Redis 密码(虽被注释,仍然暴露)
根据 PR 目标和 README.md(第 86 行)的明确说明:"不要提交 config/config.yaml
"。
必须立即采取的措施:
- 从此 PR 中移除
config/config.yaml
文件 - 将其添加到
.gitignore
(如果尚未添加) - 使用
config.example.yaml
作为模板,用户应在本地复制并填入真实值 - 如果已经提交到历史记录,考虑使用
git filter-branch
或 BFG Repo-Cleaner 清理历史 - 立即轮换所有已暴露的密钥和密码
参考 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.
"/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" | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
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_id
和 full
参数。
建议定义专用的请求体模型:
在 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.
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" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.
/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 |
There was a problem hiding this comment.
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_id
和 full
字段。
在 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
严重安全漏洞:升级 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.
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.
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, | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DSN 中的密码可能在日志中泄露。
虽然 Line 46 的日志没有打印密码,但 GORM 的内部日志可能记录完整的 DSN 字符串(包含密码)。
建议:
- 确保 GORM 日志级别设置为
logger.Warn
或更高(Line 32 已设置,很好) - 在生产环境中使用更严格的日志级别
- 考虑使用环境变量或密钥管理服务传递敏感配置
参考 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"` |
There was a problem hiding this comment.
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
标签的 OnDelete
和 OnUpdate
选项,但缺少对 Conversation
表的显式外键声明。
GORM 的 constraint
标签需要配合外键关系使用。请验证:
- 是否在
Conversation
模型中定义了反向关联? - 数据库是否正确创建了带 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.
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 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
不要忽略创建会话失败的错误
当新建会话(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.
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.
// 会话归属验证(如果会话存在则检查 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 | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
会话不存在时应及时返回错误
目前如果 db.Global.First
找不到会话(Line 305-313),代码会悄悄继续执行,后续写消息时由于 models.Message
在 internal/models/message.go
中声明了外键约束,会直接写库失败并被忽略,导致整个会话既不存在又无法正确保存。建议区分“未找到”与“查询失败”,对前者返回 404/403,对后者返回 500,并在文件顶部补充 errors
与 gorm.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.
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 | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
持久化消息失败时必须立即终止
无论是写入用户消息还是助手回复(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.
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.
内容
/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