Skip to content

Conversation

@ZaeXT
Copy link

@ZaeXT ZaeXT commented Oct 22, 2025

Summary by CodeRabbit

  • 新功能

    • 完整前端界面:聊天工作区、侧栏、消息列表、输入框、模型页、分类管理、回收站、个人资料与设置
    • 实时流式聊天(SSE)、“思考”模式、多轮上下文与模型分级权限
    • 主题与多语言支持、通知/吐司、代码/Markdown 渲染与复制
  • 文档 & 测试

    • 补充中文项目与前端 README;新增多套端到端与场景测试脚本
  • 杂项

    • 新增忽略与环境示例配置文件(.gitignore/.env 等)

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 22, 2025

Walkthrough

添加完整后端与前端实现:配置加载、数据库初始化、GORM 模型与仓储、服务层、Gin HTTP 处理器(含 JWT 中间件)、Volcengine 流式适配器与 SSE 支持、定时任务、前端 Vue 3 应用、以及多份端到端与集成测试脚本与文档。

Changes

Cohort / File(s) 变更摘要
仓库根与模块/忽略
\.gitignore, ZaeXT/go.mod
添加忽略规则与新 Go module(依赖声明)
配置示例与初始化
ZaeXT/configs/config.yaml.example, ZaeXT/internal/configs/config.go
新增示例 config 和 Viper-based 配置结构与 Init() 加载/校验
后端启动与路由
ZaeXT/cmd/server/main.go, ZaeXT/internal/handler/router.go, ZaeXT/internal/tasks/cron.go
服务引导(配置/DB/仓储/服务/cron/HTTP/优雅关停)与 Gin 路由注册与 cron 任务启动
适配器与流式 AI
ZaeXT/internal/adapter/volcengine/doubao_client.go
新增 VolcengineAdapter:模型分级权限、ChatStream 流式实现与流解析
模型(GORM)
ZaeXT/internal/model/*
.../base.go, .../user.go, .../conversation.go, .../message.go, .../category.go
新增 BaseModel 与 User/Conversation/Message/Category 实体(关联、索引、软删、级联)
仓储层(DB)
ZaeXT/internal/repository/db/init.go, .../user_repo.go, .../conversation_repo.go, .../message_repo.go, .../category_repo.go, .../repository.go
DB 初始化(GORM+MySQL)、各实体仓储接口与实现、事务与永久删除/级联逻辑、仓储聚合器
服务层
ZaeXT/internal/service/*
.../user_service.go, .../chat_service.go, .../category_service.go, .../recycle_bin_service.go, .../service.go
用户/聊天/分类/回收站服务,新建 ChatService 支持流式 AIAdapter、自动标题/分类与回收站清理
HTTP 处理器与中间件
ZaeXT/internal/handler/*
.../user_handler.go, .../chat_handler.go, .../category_handler.go, .../recycle_bin_handler.go, .../middleware/auth.go, .../request/*, .../response/*
新增各资源 Handler、请求/响应 DTO、统一 Response 工具与 JWT Auth 中间件,ChatHandler 提供 SSE 转发
公用包与工具
ZaeXT/internal/pkg/*
.../e/code.go, .../hash/bcrypt.go, .../jwt/jwt.go, .../streaming/sse.go
添加错误码映射、bcrypt 工具、JWT 生成/解析、SSE 事件格式化/发送
前端项目与配置
ZaeXT/frontend/*
package.json, vite.config.ts, tsconfig.*, tailwind.config.ts, .eslintrc.cjs, .prettierrc.cjs, 等
新增完整 Vue 3 + TypeScript 前端项目配置、lint/format/test 配置与环境示例
前端代码(API / stores / 视图 / 组件)
ZaeXT/frontend/src/api/*, .../stores/*, .../views/*, .../components/*, .../types/*, .../i18n/*
新增 HTTP 客户端/ SSE 客户端、各类 API 封装、Pinia stores(auth/model/category/conversation/recycle-bin)、大量 UI 组件与视图、类型定义与 i18n 资源
前端测试与工具
ZaeXT/frontend/src/api/__tests__/*, vite/vitest setup
新增前端单元/集成测试用例与测试启动配置
端到端与集成测试脚本(Python)
ZaeXT/test-scripts/*
test_backend.py, test_cascade_delete.py, test_failures.py, test_premissions.py
新增多份 Python 脚本覆盖注册/登录、模型权限、流式聊天、级联删除、回收站与失败场景
文档
ZaeXT/README.md, ZaeXT/frontend/README.md
新增后端与前端中文 README,包含架构、启动、API 与使用说明

Sequence Diagram(s)

sequenceDiagram
    participant Client as 客户端
    participant API as HTTP Handler
    participant Auth as AuthMiddleware
    participant Service as Service 层
    participant Repo as Repository 层
    participant AI as VolcengineAdapter
    participant DB as 数据库

    Client->>API: 请求(带/不带 Bearer)
    API->>Auth: 验证 Token (若需)
    Auth-->>API: userID, userTier
    API->>Service: 发起业务请求(如 ProcessUserMessage)
    Service->>Repo: 查询/写入 会话/消息/用户/分类
    Repo->>DB: 执行 SQL/Migrations
    Service->>AI: ChatStream(req, userTier, modelID)
    AI->>AI: 校验模型权限并发起流式请求
    AI-->>Service: 流式数据块(chunks)
    Service-->>API: 通过通道转发流式内容
    API->>Client: SSE 流(data 事件分块)
    note over Service,Repo: 完成后持久化助手消息并可能触发自动标题/分类/回收清理
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

关注点(建议在审查时重点检查):

  • conversation_repo.go 与 category_repo.go:永久删除、事务、递归 CTE 与级联逻辑的正确性与 SQL 安全性;
  • chat_service.go 与 AIAdapter/volcengine doubao_client.go:流式处理、错误/取消传播、模型权限验证与并发行为;
  • db/init.go 与 migrations:连接参数、AutoMigrate 的作用域与生产环境影响;
  • handler/chat_handler.go 与 streaming SSE 实现:SSE 头/断开/超时与客户端交互契约;
  • frontend src/api/sse.ts 与前端 streaming 控制器:流解析、超时、Abort 信号与 UI 同步;
  • 测试脚本(Python 与前端测试):网络请求假设、环境依赖与脆弱断言。

Possibly related PRs

  • Add: KimmyXYC LLM Backend #8 — 具有代码层面重叠:后端启动、DB init、JWT 中间件、模型/仓储、路由与 SSE/流式聊天相关实现,可能引入冲突或可复用实现片段。

Poem

🐰 兔子在键盘上轻敲声响,
新后端与前端齐步登场。
对话如泉涌,分类与回收按时忙,
模型分级有序,流式响应不慌张。
README 昭示路,测试脚本护航。

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive PR标题"提交任务"是一个非常宽泛且通用的术语,完全没有传达关于本次变更内容的具体信息。根据摘要,该PR包含了一个完整的AI对话系统实现,涵盖后端Go应用(包括身份验证、对话管理、分类、回收站、模型访问控制等功能)和前端Vue 3应用。这个标题无法帮助开发者快速理解PR的主要改动,违反了"清晰、具体"的命名原则。 建议将PR标题改为更具体和描述性的内容,例如"实现AI对话系统:完整的后端和前端实现"或"Add complete AI chat application with backend and frontend"。新标题应该清楚地反映本次变更的核心内容,使审阅者和未来的开发者能够快速理解这个PR的主要目的。
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ 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.

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: 32

🧹 Nitpick comments (35)
ZaeXT/README.md (2)

1-104: 修复 Markdown 格式化问题

文档中存在多处 Markdown 格式化违规,需要统一调整:

  1. 列表缩进不一致(MD007):整个文件中的无序列表项使用了 4 个空格缩进,而 markdownlint 的默认配置要求为 2 个空格。这影响超过 50 行的内容,包括所有特性描述、API 文档部分。

  2. 代码块缺少语言标注(MD040):第 78 行的项目结构代码块应指定语言类型以便正确渲染。

建议修复这些格式化问题,以确保文档在不同 Markdown 渲染器中的一致显示。

请考虑应用以下修复:

-   **用户账户系统**:
-    -   **用户注册与登录**(密码使用 bcrypt 加密)。
-    -   基于 **JWT (JSON Web Token)** 的无状态 API 认证。
+  - **用户账户系统**:
+    - **用户注册与登录**(密码使用 bcrypt 加密)。
+    - 基于 **JWT (JSON Web Token)** 的无状态 API 认证。

对于代码块(第 78 行),添加语言标签:

-```
+``` text
 .
 ├── cmd/server/            # 程序主入口

109-115: 语法优化建议(可选)

第 114 行的表述「这种清晰的分层使得...」中,"清晰的" 作修饰语,按照中文表达习惯,建议改为"清晰地分层"或重新表述:

-这种清晰的分层使得每一层都可以被独立测试,极大地提高了代码质量和可维护性。
+这种清晰地分层使得每一层都可以被独立测试,极大地提高了代码质量和可维护性。

-这种清晰的分层使得每一层都可以被独立测试,极大地提高了代码质量和可维护性。
+这样清晰的分层设计使得每一层都可以被独立测试,极大地提高了代码质量和可维护性。
ZaeXT/go.mod (1)

1-1: 模块路径应遵循标准Go模块命名约定。

根据Go模块的最佳实践,模块路径应包含域名或组织前缀,例如 github.com/hduhelp/backend_2025_freshman_taskgithub.com/hduhelp/ai-qa-backend,而不仅仅是 ai-qa-backend。这样可以避免导入路径冲突,并与GitHub仓库结构保持一致。

建议将模块路径调整为:

-module ai-qa-backend
+module github.com/hduhelp/backend_2025_freshman_task

或者,如果希望将此作为独立模块:

-module ai-qa-backend
+module github.com/hduhelp/ai-qa-backend
.gitignore (1)

1-2: 忽略规则合理

本地私有配置与 macOS 元数据应被忽略,变更合理。另建议保留示例配置在仓库中(如 ZaeXT/configs/config.yaml.example),并在 README 引导复制为本地 config.yaml,避免误提交真实密钥。也可考虑忽略常见本地文件(.env、/logs、/tmp 等)。

ZaeXT/internal/pkg/streaming/sse.go (1)

33-35: 发送端的冲刷与响应头由上层 HTTP 处理器保证

请在使用处确保设置正确响应头并逐事件冲刷:

  • Content-Type: text/event-stream
  • Cache-Control: no-cache
  • Connection: keep-alive
  • X-Accel-Buffering: no(如走反向代理)
    并在每次写入后调用 http.Flusher.Flush()。这属于 handler 层职责,但请在包注释或函数注释中注明。
ZaeXT/test-scripts/test_failures.py (1)

116-116: 小问题:无占位的 f-string 与宽泛异常捕获

  • 去掉无占位的 f 前缀(行 116、120)。
  • 主流程中尽量捕获更具体的异常,如 requests.RequestException,保留最后兜底的 Exception。

Also applies to: 120-120, 182-183

ZaeXT/internal/pkg/jwt/jwt.go (1)

34-48: 统一时间基线,避免多次取 Now 带来微小漂移

建议一次取 now := time.Now().UTC(),用于 iat/nbf/exp,保证一致性:

 func (j *JWT) GenerateToken(userID uint, username, userTier string, expiration time.Duration) (string, error) {
-  claims := CustomClaims{
+  now := time.Now().UTC()
+  claims := CustomClaims{
     UserID:   userID,
     Username: username,
     UserTier: userTier,
     RegisteredClaims: jwt.RegisteredClaims{
-      IssuedAt:  jwt.NewNumericDate(time.Now()),
-      NotBefore: jwt.NewNumericDate(time.Now()),
-      ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiration)),
+      IssuedAt:  jwt.NewNumericDate(now),
+      NotBefore: jwt.NewNumericDate(now),
+      ExpiresAt: jwt.NewNumericDate(now.Add(expiration)),
       Issuer:    "ai-qa-system",
     },
   }
ZaeXT/test-scripts/test_cascade_delete.py (1)

10-37: 建议将通用辅助函数提取到共享模块。

这些辅助函数(random_stringprint_test_headerprint_successprint_failget_auth_headersregister_and_login)在多个测试脚本中重复出现(test_backend.py、test_premissions.py、test_failures.py)。

建议创建一个共享的测试工具模块(例如 test_utils.py),将这些通用函数提取到其中:

# test_utils.py
import requests
import random
import string

def random_string(length=8):
    return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length))

def print_test_header(title):
    print("\n" + "="*50)
    print(f"  🧪  开始测试: {title}")
    print("="*50)

# ... 其他通用函数

然后在各个测试脚本中导入:

from test_utils import random_string, print_test_header, print_success, print_fail, get_auth_headers, register_and_login
ZaeXT/internal/handler/request/category.go (1)

3-11: DTO 定义正确,可选考虑合并重复结构。

CreateCategoryUpdateCategory 的字段和验证规则完全相同。当前的分离提供了语义清晰性,但也引入了代码重复。

如果未来需要为创建和更新操作添加不同的验证规则或字段,保持分离是合理的。但如果两者始终保持一致,可以考虑合并为单一类型:

type CategoryRequest struct {
    Name     string `json:"name" binding:"required,max=100"`
    ParentID *uint  `json:"parent_id"`
}

或使用类型别名以保持语义清晰:

type CategoryRequest struct {
    Name     string `json:"name" binding:"required,max=100"`
    ParentID *uint  `json:"parent_id"`
}

type CreateCategory = CategoryRequest
type UpdateCategory = CategoryRequest
ZaeXT/cmd/server/main.go (1)

58-62: 考虑为 cron 停止设置超时

当前实现中,cron 调度器的停止操作(Line 60-62)会无限期等待 cronCtx.Done(),但 HTTP 服务器的关闭超时设置为 5 秒(Line 58)。如果 cron 任务运行时间过长,可能会延迟整个关闭流程。

考虑为 cron 停止操作也添加超时控制:

 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 	defer cancel()
+
 	cronCtx := cronScheduler.Stop()
-	<-cronCtx.Done()
-	log.Println("Cron scheduler stopped.")
+	select {
+	case <-cronCtx.Done():
+		log.Println("Cron scheduler stopped.")
+	case <-ctx.Done():
+		log.Println("Cron scheduler stop timed out.")
+	}
ZaeXT/internal/model/message.go (1)

3-10: 考虑为 Role 字段添加文档注释

Role 字段通常有固定的取值(如 "user"、"assistant"、"system"),建议添加注释说明允许的值,以提高代码可维护性。

 type Message struct {
 	BaseModel
 	ConversationID uint   `gorm:"not null;index"`
+	// Role 表示消息的角色,可选值:user, assistant, system
 	Role           string `gorm:"size:20;not null"`
 	Content        string `gorm:"type:text;not null"`
 
 	Conversation Conversation `gorm:"foreignKey:ConversationID"`
 }
ZaeXT/internal/model/category.go (1)

3-11: 考虑添加循环引用保护

当前的分类模型支持父子层级结构,但没有防止循环引用的约束。虽然这可以在业务逻辑层处理,但建议在创建/更新分类时验证父级关系,避免出现循环引用或过深的层级结构。

可以在 CategoryService 的创建/更新方法中添加验证逻辑:

// 验证父级分类是否会形成循环引用
func (s *categoryService) validateParentID(categoryID, parentID uint) error {
    // 检查 parentID 的祖先链中是否包含 categoryID
    // 实现省略
}
ZaeXT/internal/tasks/cron.go (1)

10-29: Cron 任务调度实现正确

定时任务的实现整体良好:

  • 使用秒级精度的 cron 表达式("0 0 4 * * *")准确设置每日凌晨 4 点执行
  • 错误处理和日志记录完善
  • AddFunc 失败时使用 Fatal 确保问题被及时发现

轻微建议:Line 26 的 go c.Start() 中的 go 关键字可以省略,因为 robfig/cron/v3 的 Start() 方法本身就是非阻塞的,会在内部启动 goroutine。

-	go c.Start()
+	c.Start()
 	log.Println("Cron job scheduler started.")
ZaeXT/internal/repository/db/init.go (2)

54-56: 连接池参数应该可配置并验证

发现两个改进点:

  1. ConnMaxLifetime 被硬编码为 1 小时,建议从配置文件读取以提高灵活性
  2. 缺少对连接池参数的验证,应确保 MaxIdleConns <= MaxOpenConns

在配置结构中添加 ConnMaxLifetime 字段,并在设置前验证参数:

+	if cfg.MaxIdleConns > cfg.MaxOpenConns {
+		log.Printf("Warning: MaxIdleConns (%d) > MaxOpenConns (%d), adjusting MaxIdleConns", 
+			cfg.MaxIdleConns, cfg.MaxOpenConns)
+		cfg.MaxIdleConns = cfg.MaxOpenConns
+	}
+
 	sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
 	sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
-	sqlDB.SetConnMaxLifetime(time.Hour)
+	sqlDB.SetConnMaxLifetime(time.Duration(cfg.ConnMaxLifetime) * time.Second)

42-47: 建议添加连接验证

成功打开数据库连接后,建议调用 sqlDB.Ping() 来验证连接是否真正可用,这样可以在启动阶段就发现连接问题,而不是等到第一次查询时才失败。

 	sqlDB, err := db.DB()
 	if err != nil {
 		return nil, fmt.Errorf("failed to get sql.DB: %w", err)
 	}
+
+	if err := sqlDB.Ping(); err != nil {
+		return nil, fmt.Errorf("failed to ping database: %w", err)
+	}
 
 	sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
ZaeXT/internal/handler/request/user.go (1)

3-6: 建议加强密码策略。

当前密码最小长度为 6 个字符,这在现代安全标准中较为薄弱。建议考虑:

  • 将最小长度提升至 8-12 个字符
  • 可选:增加密码复杂度要求(大小写、数字、特殊字符)

另外,Username 的 max 设置为 50,但数据库模型中定义的 size 为 64,存在轻微不一致。虽然不会造成问题,但建议统一为相同值。

-	Username string `json:"username" binding:"required,min=3,max=50"`
-	Password string `json:"password" binding:"required,min=6,max=50"`
+	Username string `json:"username" binding:"required,min=3,max=64"`
+	Password string `json:"password" binding:"required,min=8,max=50"`
ZaeXT/internal/handler/category_handler.go (1)

22-44: 建议增加上下文值的防御性检查。

虽然认证中间件应该确保 userID 存在,但添加错误检查可以提高代码健壮性,防止中间件逻辑变更时出现运行时 panic。

 func (h *CategoryHandler) Create(c *gin.Context) {
-	userID, _ := c.Get("userID")
+	userID, exists := c.Get("userID")
+	if !exists {
+		response.Fail(c, e.Unauthorized, "用户未认证")
+		return
+	}
 
 	var req request.CreateCategory
 	if err := c.ShouldBindJSON(&req); err != nil {
 		response.Fail(c, e.InvalidParams, err.Error())
 		return
 	}
 
-	category, err := h.categoryService.Create(userID.(uint), req.Name, req.ParentID)
+	uid, ok := userID.(uint)
+	if !ok {
+		response.Fail(c, e.Error, "无效的用户ID类型")
+		return
+	}
+	category, err := h.categoryService.Create(uid, req.Name, req.ParentID)

注:其他方法(List, Update, Delete)也有相同模式,建议统一处理。

ZaeXT/internal/repository/user_repo.go (1)

28-32: 可引入上下文以支持超时/取消

仓储接口与实现均未接收 context.Context。为提高可观测性与超时控制,建议为接口方法增加 ctx,并在 GORM 调用链使用 WithContext(ctx)。

示例:

type UserRepository interface {
    Create(ctx context.Context, user *model.User) error
    GetByUsername(ctx context.Context, username string) (*model.User, error)
    ...
}
ZaeXT/internal/repository/category_repo.go (2)

49-75: 删除实现依赖 MySQL 8 递归 CTE 与 FK 级联,请明确前置条件或提供降级策略

  • 使用 WITH RECURSIVE 需要 MySQL≥8.0;低版本将直接报错。
  • 真正删除仅针对根节点,依赖 categories(ParentID) 上的 ON DELETE CASCADE 才能清理子孙;若迁移未正确下发 FK/级联,可能遗留孤儿节点。

建议:

  • 文档/启动检查声明并校验 MySQL 版本;或提供无需 CTE 的回退逻辑(多次查询/循环收集后删除)。
  • 确认 AutoMigrate 已创建 ParentID 外键且带 ON DELETE CASCADE;否则应显式对 idsToDelete 由叶到根顺序删除。
    可用脚本检查 SQL 版本与表结构(本地/CI 环境)后再决定是否走 CTE 路径。

35-39: 仅预加载一层 Children,深层级场景可能不够

当前 Preload("Children") 只加载一层。若前端需要多层树,考虑递归加载(多次 Preload)或服务层聚合递归。

ZaeXT/internal/handler/user_handler.go (2)

29-31: 注册错误码与消息应区分“用户名已存在”,且勿回显内部错误

目前一概 500 + err.Error()。建议识别“用户名已存在”并返回 400,且对外统一文案,内部错误打日志即可。

- if err := h.userService.Register(req.Username, req.Password); err != nil {
-   response.Fail(c, e.Error, err.Error())
+ if err := h.userService.Register(req.Username, req.Password); err != nil {
+   if errors.Is(err, service.ErrUsernameExists) {
+     response.Fail(c, e.InvalidParams, "用户名已存在")
+   } else {
+     response.Fail(c, e.Error, "注册失败")
+   }
    return
 }

需要在 service 定义并返回 service.ErrUsernameExists(见对 user_service.go 的建议)。


44-51: 登录错误信息最小化,避免暴露细节;区分鉴权失败与系统错误

对外建议固定“用户名或密码错误”,仅系统错误返回 500。需 service 提供 ErrInvalidCredentials

-token, err := h.userService.Login(req.Username, req.Password)
-if err != nil {
-  response.Fail(c, e.Unauthorized, err.Error())
+token, err := h.userService.Login(req.Username, req.Password)
+if err != nil {
+  if errors.Is(err, service.ErrInvalidCredentials) {
+    response.Fail(c, e.Unauthorized, "用户名或密码错误")
+  } else {
+    response.Fail(c, e.Error, "登录失败")
+  }
   return
 }
ZaeXT/test-scripts/test_premissions.py (2)

36-45: 为所有 HTTP 请求添加超时并正确关闭流式响应

当前无 timeout,且流式响应未关闭,可能导致挂死与资源泄露。

+DEFAULT_TIMEOUT = 10  # seconds
...
- reg_res = requests.post(f"{BASE_URL}/register", json={"username": username, "password": password})
+ reg_res = requests.post(f"{BASE_URL}/register", json={"username": username, "password": password}, timeout=DEFAULT_TIMEOUT)
...
- login_res = requests.post(f"{BASE_URL}/login", json={"username": username, "password": password})
+ login_res = requests.post(f"{BASE_URL}/login", json={"username": username, "password": password}, timeout=DEFAULT_TIMEOUT)
...
- res_free_models = requests.get(f"{BASE_URL}/models", headers=get_auth_headers(free_user_token))
+ res_free_models = requests.get(f"{BASE_URL}/models", headers=get_auth_headers(free_user_token), timeout=DEFAULT_TIMEOUT)
...
- res_premium_models = requests.get(f"{BASE_URL}/models", headers=get_auth_headers(premium_user_token))
+ res_premium_models = requests.get(f"{BASE_URL}/models", headers=get_auth_headers(premium_user_token), timeout=DEFAULT_TIMEOUT)
...
- res_conv = requests.post(f"{BASE_URL}/conversations", headers=get_auth_headers(free_user_token), json={})
+ res_conv = requests.post(f"{BASE_URL}/conversations", headers=get_auth_headers(free_user_token), json={}, timeout=DEFAULT_TIMEOUT)
...
- res_permission_denied = requests.post(f"{BASE_URL}/conversations/{conv_id}/messages", 
-                                       headers=get_auth_headers(free_user_token),
-                                       json=chat_payload)
+ res_permission_denied = requests.post(f"{BASE_URL}/conversations/{conv_id}/messages",
+                                       headers=get_auth_headers(free_user_token),
+                                       json=chat_payload,
+                                       timeout=DEFAULT_TIMEOUT)
...
- res_conv_prem = requests.post(f"{BASE_URL}/conversations", headers=get_auth_headers(premium_user_token), json={})
+ res_conv_prem = requests.post(f"{BASE_URL}/conversations", headers=get_auth_headers(premium_user_token), json={}, timeout=DEFAULT_TIMEOUT)
...
- res_permission_ok = requests.post(f"{BASE_URL}/conversations/{conv_id_prem}/messages",
-                                   headers=get_auth_headers(premium_user_token),
-                                   json=chat_payload_prem,
-                                   stream=True)
+ with requests.post(f"{BASE_URL}/conversations/{conv_id_prem}/messages",
+                    headers=get_auth_headers(premium_user_token),
+                    json=chat_payload_prem,
+                    stream=True,
+                    timeout=DEFAULT_TIMEOUT) as res_permission_ok:
+     if res_permission_ok.status_code == 200:
+         print_success("'premium' 用户使用 'premium' 模型请求成功 (200 OK)")
+     else:
+         print_fail("'premium' 用户使用 'premium' 模型失败", res_permission_ok)

Also applies to: 66-76, 81-91, 98-116, 122-139, 131-135


78-78: 移除未使用变量

free_model_id 未使用,可删除。

- free_model_id = free_models[0]['id']
ZaeXT/internal/service/user_service.go (1)

100-112: 默认分类创建失败仅打印日志是否符合需求?

如果默认分类是产品承诺的一部分,建议事务包裹或将错误上抛;否则保留日志即可。

ZaeXT/internal/handler/chat_handler.go (3)

135-146: 基于字符串的错误分类脆弱,使用可判别错误或 error 类型

strings.Contains("permission denied"/"not found") 易脆弱、易国际化失配。建议 service 返回哨兵错误(如 ErrPermissionDenied/ErrNotFound),这里用 errors.Is 判断。


152-157: SSE 响应建议补充代理相关头,减少缓冲影响

可增加 X-Accel-Buffering: noCache-Control: no-transform,提升在 Nginx/CDN 下的可用性。

 c.Writer.Header().Set("Content-Type", "text/event-stream")
 c.Writer.Header().Set("Cache-Control", "no-cache")
+c.Writer.Header().Set("Cache-Control", "no-transform")
 c.Writer.Header().Set("Connection", "keep-alive")
+c.Writer.Header().Set("X-Accel-Buffering", "no")
- c.Writer.Header().Set("Access-Control-Allow-Origin", "*")

CORS 建议在中间件统一处理,避免此处覆盖全局策略。


118-129: ProcessMessage 建议将请求上下文传入 service 以便取消下游协程

当前仅退出 handler 循环,service 内部可能泄漏 goroutine。建议扩展 ChatService.ProcessUserMessage(ctx, ...)

我可协助出接口与实现改动草案。

ZaeXT/internal/repository/conversation_repo.go (1)

79-109: 批量永久删除存在潜在大结果集加载风险,建议分批处理

当前一次性加载所有 ID 到内存。可按分页/分批 IDs 处理,避免内存峰值。

ZaeXT/internal/repository/message_repo.go (2)

27-36: 批量插入应使用 CreateInBatches,避免 N 次往返

当前逐条 tx.Create 会造成大量往返与锁竞争,影响高并发写入性能;同时未处理空切片。建议一次性批量插入并对空输入快速返回。

 func (r *messageRepository) CreateBatch(messages []*model.Message) error {
-	return r.db.Transaction(func(tx *gorm.DB) error {
-		for _, msg := range messages {
-			if err := tx.Create(msg).Error; err != nil {
-				return err
-			}
-		}
-		return nil
-	})
+	if len(messages) == 0 {
+		return nil
+	}
+	return r.db.Transaction(func(tx *gorm.DB) error {
+		const batchSize = 500
+		return tx.CreateInBatches(messages, batchSize).Error
+	})
 }

38-42: 结果排序建议加入次级键,保证稳定性

仅按 created_at ASC 在同秒级写入可能出现并列,读取顺序不稳定。建议追加 id ASC 作为次级排序键。

-err := r.db.Where("conversation_id = ?", convID).Order("created_at asc").Find(&messages).Error
+err := r.db.Where("conversation_id = ?", convID).
+	Order("created_at ASC, id ASC").
+	Find(&messages).Error
ZaeXT/test-scripts/test_backend.py (1)

8-10: 为所有 HTTP 请求增加超时并支持可配置 BASE_URL

当前所有 requests.* 无超时,容易在网络抖动/服务无响应时挂死 CI。建议:引入统一的超时与请求封装,并将 BASE_URL 支持从环境变量覆盖。

@@
-import requests
+import requests
+import os
@@
-# --- 配置 ---
-BASE_URL = "http://localhost:8080/api/v1"
+# --- 配置 ---
+BASE_URL = os.getenv("BASE_URL", "http://localhost:8080/api/v1")
+DEFAULT_TIMEOUT = (5, 60)  # (connect_timeout, read_timeout)
@@
 def get_auth_headers():
@@
     return {"Authorization": f"Bearer {auth_token}", "Content-Type": "application/json"}
+
+# 统一请求封装:默认超时,可按需在调用处覆盖
+def rq(method: str, url: str, **kwargs):
+    kwargs.setdefault("timeout", DEFAULT_TIMEOUT)
+    return requests.request(method=method, url=url, **kwargs)
@@
-    res = requests.post(f"{BASE_URL}/register", json=user_credentials)
+    res = rq("POST", f"{BASE_URL}/register", json=user_credentials)
@@
-    res = requests.post(f"{BASE_URL}/login", json=user_credentials)
+    res = rq("POST", f"{BASE_URL}/login", json=user_credentials)
@@
-    res = requests.get(f"{BASE_URL}/profile", headers=get_auth_headers())
+    res = rq("GET", f"{BASE_URL}/profile", headers=get_auth_headers())
@@
-    res = requests.put(f"{BASE_URL}/profile/memory", headers=get_auth_headers(), json={"memory_info": memory_info})
+    res = rq("PUT", f"{BASE_URL}/profile/memory", headers=get_auth_headers(), json={"memory_info": memory_info})
@@
-    res = requests.get(f"{BASE_URL}/profile", headers=get_auth_headers())
+    res = rq("GET", f"{BASE_URL}/profile", headers=get_auth_headers())
@@
-        response = requests.post(f"{BASE_URL}/conversations/{conversation_id}/messages", 
-                                 headers=get_auth_headers(), 
-                                 json=chat_payload, 
-                                 stream=True)
+        # SSE 场景建议更长的读取超时
+        response = rq("POST", f"{BASE_URL}/conversations/{conversation_id}/messages",
+                      headers=get_auth_headers(),
+                      json=chat_payload,
+                      stream=True,
+                      timeout=(5, 300))

Also applies to: 41-46, 60-65, 68-74, 77-83, 87-92, 95-100, 148-156

ZaeXT/internal/adapter/volcengine/doubao_client.go (2)

159-161: 为流式接口补充标准 SSE 头

建议补充 Accept: text/event-streamCache-Control: no-cacheConnection: keep-alive,提高兼容性。

 		httpReq.Header.Set("Content-Type", "application/json")
 		httpReq.Header.Set("Authorization", "Bearer "+a.apiKey)
+		httpReq.Header.Set("Accept", "text/event-stream")
+		httpReq.Header.Set("Cache-Control", "no-cache")
+		httpReq.Header.Set("Connection", "keep-alive")

74-79: Tier 比较应大小写无关,避免配置/入参大小写不一致

userTier 进行 strings.ToLower 归一化,减少因大小写导致的权限误判。

 func (a *VolcengineAdapter) GetAvailableModelsForTier(userTier string) []AvailableModel {
-	userLevel, ok := a.tierLevels[userTier]
+	userLevel, ok := a.tierLevels[strings.ToLower(userTier)]
@@
 func (a *VolcengineAdapter) isValidModelForTier(modelID, userTier string) bool {
-	userLevel, ok := a.tierLevels[userTier]
+	userLevel, ok := a.tierLevels[strings.ToLower(userTier)]

Also applies to: 96-101

ZaeXT/internal/service/chat_service.go (1)

13-16: 服务层类型不应依赖具体适配器包类型(解耦建议)

AIAdapter 方法签名直接使用 volcengine 包内类型,增加耦合。建议在 service 层自定义最小必要的 DTO 接口/结构体,由适配器实现转换。

我可以补一个最小 DTO 与适配器转换示例,若需要请告知。

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cc0e02d and 223af49.

⛔ Files ignored due to path filters (1)
  • ZaeXT/go.sum is excluded by !**/*.sum
📒 Files selected for processing (45)
  • .gitignore (1 hunks)
  • ZaeXT/README.md (1 hunks)
  • ZaeXT/cmd/server/main.go (1 hunks)
  • ZaeXT/configs/config.yaml.example (1 hunks)
  • ZaeXT/go.mod (1 hunks)
  • ZaeXT/internal/adapter/volcengine/doubao_client.go (1 hunks)
  • ZaeXT/internal/configs/config.go (1 hunks)
  • ZaeXT/internal/handler/category_handler.go (1 hunks)
  • ZaeXT/internal/handler/chat_handler.go (1 hunks)
  • ZaeXT/internal/handler/middleware/auth.go (1 hunks)
  • ZaeXT/internal/handler/recycle_bin_handler.go (1 hunks)
  • ZaeXT/internal/handler/request/category.go (1 hunks)
  • ZaeXT/internal/handler/request/chat.go (1 hunks)
  • ZaeXT/internal/handler/request/user.go (1 hunks)
  • ZaeXT/internal/handler/response/category.go (1 hunks)
  • ZaeXT/internal/handler/response/chat.go (1 hunks)
  • ZaeXT/internal/handler/response/response.go (1 hunks)
  • ZaeXT/internal/handler/response/user.go (1 hunks)
  • ZaeXT/internal/handler/router.go (1 hunks)
  • ZaeXT/internal/handler/user_handler.go (1 hunks)
  • ZaeXT/internal/model/base.go (1 hunks)
  • ZaeXT/internal/model/category.go (1 hunks)
  • ZaeXT/internal/model/conversation.go (1 hunks)
  • ZaeXT/internal/model/message.go (1 hunks)
  • ZaeXT/internal/model/user.go (1 hunks)
  • ZaeXT/internal/pkg/e/code.go (1 hunks)
  • ZaeXT/internal/pkg/hash/bcrypt.go (1 hunks)
  • ZaeXT/internal/pkg/jwt/jwt.go (1 hunks)
  • ZaeXT/internal/pkg/streaming/sse.go (1 hunks)
  • ZaeXT/internal/repository/category_repo.go (1 hunks)
  • ZaeXT/internal/repository/conversation_repo.go (1 hunks)
  • ZaeXT/internal/repository/db/init.go (1 hunks)
  • ZaeXT/internal/repository/message_repo.go (1 hunks)
  • ZaeXT/internal/repository/repository.go (1 hunks)
  • ZaeXT/internal/repository/user_repo.go (1 hunks)
  • ZaeXT/internal/service/category_service.go (1 hunks)
  • ZaeXT/internal/service/chat_service.go (1 hunks)
  • ZaeXT/internal/service/recycle_bin_service.go (1 hunks)
  • ZaeXT/internal/service/service.go (1 hunks)
  • ZaeXT/internal/service/user_service.go (1 hunks)
  • ZaeXT/internal/tasks/cron.go (1 hunks)
  • ZaeXT/test-scripts/test_backend.py (1 hunks)
  • ZaeXT/test-scripts/test_cascade_delete.py (1 hunks)
  • ZaeXT/test-scripts/test_failures.py (1 hunks)
  • ZaeXT/test-scripts/test_premissions.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (33)
ZaeXT/internal/model/conversation.go (3)
ZaeXT/internal/model/base.go (1)
  • BaseModel (5-9)
ZaeXT/internal/model/user.go (1)
  • User (3-12)
ZaeXT/internal/model/message.go (1)
  • Message (3-10)
ZaeXT/internal/tasks/cron.go (1)
ZaeXT/internal/service/service.go (1)
  • Service (7-12)
ZaeXT/internal/handler/router.go (7)
ZaeXT/internal/service/service.go (1)
  • Service (7-12)
ZaeXT/internal/handler/user_handler.go (1)
  • NewUserHandler (18-20)
ZaeXT/internal/handler/chat_handler.go (1)
  • NewChatHandler (21-23)
ZaeXT/internal/handler/category_handler.go (1)
  • NewCategoryHandler (18-20)
ZaeXT/internal/handler/recycle_bin_handler.go (1)
  • NewRecycleBinHandler (18-20)
ZaeXT/internal/handler/middleware/auth.go (1)
  • AuthMiddleware (13-42)
ZaeXT/internal/handler/request/chat.go (1)
  • CreateConversation (8-11)
ZaeXT/internal/pkg/jwt/jwt.go (1)
zxstring/utils/jwt.go (1)
  • GenerateToken (20-36)
ZaeXT/internal/model/user.go (3)
ZaeXT/internal/model/base.go (1)
  • BaseModel (5-9)
ZaeXT/internal/model/conversation.go (1)
  • Conversation (5-16)
ZaeXT/internal/model/category.go (1)
  • Category (3-11)
ZaeXT/internal/service/category_service.go (2)
ZaeXT/internal/model/category.go (1)
  • Category (3-11)
ZaeXT/internal/repository/category_repo.go (1)
  • CategoryRepository (9-15)
ZaeXT/cmd/server/main.go (7)
ZaeXT/internal/configs/config.go (2)
  • Init (62-83)
  • Conf (10-10)
ZaeXT/internal/repository/db/init.go (1)
  • Init (16-69)
ZaeXT/internal/repository/repository.go (1)
  • NewRepository (12-19)
ZaeXT/internal/adapter/volcengine/doubao_client.go (1)
  • NewVolcengineAdapter (58-72)
ZaeXT/internal/service/service.go (1)
  • NewService (14-22)
ZaeXT/internal/tasks/cron.go (1)
  • StartCronJobs (10-29)
ZaeXT/internal/handler/router.go (1)
  • SetupRouter (12-54)
ZaeXT/internal/handler/recycle_bin_handler.go (4)
ZaeXT/internal/service/recycle_bin_service.go (1)
  • RecycleBinService (11-16)
ZaeXT/internal/handler/response/response.go (2)
  • Fail (28-38)
  • Success (20-26)
ZaeXT/internal/pkg/e/code.go (3)
  • Error (5-5)
  • Success (4-4)
  • InvalidParams (6-6)
ZaeXT/internal/handler/response/chat.go (1)
  • ConversationInfo (5-13)
ZaeXT/internal/model/message.go (2)
ZaeXT/internal/model/base.go (1)
  • BaseModel (5-9)
ZaeXT/internal/model/conversation.go (1)
  • Conversation (5-16)
ZaeXT/internal/handler/request/chat.go (1)
ZaeXT/internal/model/message.go (1)
  • Message (3-10)
ZaeXT/internal/repository/user_repo.go (1)
ZaeXT/internal/model/user.go (1)
  • User (3-12)
ZaeXT/internal/handler/response/response.go (1)
ZaeXT/internal/pkg/e/code.go (8)
  • Success (4-4)
  • GetMsg (25-31)
  • InvalidParams (6-6)
  • Unauthorized (7-7)
  • PermissionDenied (8-8)
  • NotFound (9-9)
  • TooManyRequests (11-11)
  • Error (5-5)
ZaeXT/internal/handler/user_handler.go (5)
ZaeXT/internal/service/user_service.go (1)
  • UserService (16-21)
ZaeXT/internal/handler/request/user.go (3)
  • UserRegister (3-6)
  • UserLogin (8-11)
  • UpdateUserMemory (13-15)
ZaeXT/internal/handler/response/response.go (2)
  • Fail (28-38)
  • Success (20-26)
ZaeXT/internal/pkg/e/code.go (5)
  • InvalidParams (6-6)
  • Error (5-5)
  • Success (4-4)
  • Unauthorized (7-7)
  • NotFound (9-9)
ZaeXT/internal/handler/response/user.go (1)
  • UserProfile (5-11)
ZaeXT/internal/service/recycle_bin_service.go (3)
ZaeXT/internal/model/conversation.go (1)
  • Conversation (5-16)
ZaeXT/internal/repository/conversation_repo.go (1)
  • ConversationRepository (10-20)
ZaeXT/internal/configs/config.go (1)
  • Conf (10-10)
ZaeXT/internal/handler/category_handler.go (6)
ZaeXT/internal/service/category_service.go (1)
  • CategoryService (8-13)
ZaeXT/internal/handler/request/category.go (2)
  • CreateCategory (3-6)
  • UpdateCategory (8-11)
ZaeXT/internal/handler/response/response.go (2)
  • Fail (28-38)
  • Success (20-26)
ZaeXT/internal/pkg/e/code.go (3)
  • InvalidParams (6-6)
  • Error (5-5)
  • Success (4-4)
ZaeXT/internal/handler/response/category.go (1)
  • CategoryInfo (3-8)
ZaeXT/internal/model/category.go (1)
  • Category (3-11)
ZaeXT/internal/service/service.go (5)
ZaeXT/internal/service/user_service.go (2)
  • UserService (16-21)
  • NewUserService (30-37)
ZaeXT/internal/service/category_service.go (2)
  • CategoryService (8-13)
  • NewCategoryService (19-21)
ZaeXT/internal/service/chat_service.go (3)
  • ChatService (18-27)
  • AIAdapter (13-16)
  • NewChatService (37-51)
ZaeXT/internal/service/recycle_bin_service.go (2)
  • RecycleBinService (11-16)
  • NewRecycleBinService (22-24)
ZaeXT/internal/repository/repository.go (1)
  • Repository (5-10)
ZaeXT/internal/handler/middleware/auth.go (4)
ZaeXT/internal/pkg/jwt/jwt.go (2)
  • NewJWT (28-32)
  • JWT (17-19)
ZaeXT/internal/configs/config.go (1)
  • Conf (10-10)
ZaeXT/internal/handler/response/response.go (1)
  • Fail (28-38)
ZaeXT/internal/pkg/e/code.go (1)
  • Unauthorized (7-7)
ZaeXT/internal/repository/category_repo.go (2)
ZaeXT/internal/model/category.go (1)
  • Category (3-11)
ZaeXT/internal/model/conversation.go (1)
  • Conversation (5-16)
ZaeXT/test-scripts/test_failures.py (1)
ZaeXT/test-scripts/test_backend.py (4)
  • random_string (19-21)
  • print_test_header (23-26)
  • print_success (28-29)
  • print_fail (31-39)
ZaeXT/internal/pkg/e/code.go (1)
ZaeXT/internal/handler/response/response.go (1)
  • Success (20-26)
ZaeXT/internal/repository/message_repo.go (1)
ZaeXT/internal/model/message.go (1)
  • Message (3-10)
ZaeXT/internal/adapter/volcengine/doubao_client.go (2)
ZaeXT/internal/model/message.go (1)
  • Message (3-10)
ZaeXT/internal/configs/config.go (1)
  • ModelInfo (41-45)
ZaeXT/internal/service/chat_service.go (7)
ZaeXT/internal/adapter/volcengine/doubao_client.go (2)
  • ChatRequest (40-43)
  • AvailableModel (45-48)
ZaeXT/internal/model/conversation.go (1)
  • Conversation (5-16)
ZaeXT/internal/repository/conversation_repo.go (1)
  • ConversationRepository (10-20)
ZaeXT/internal/repository/message_repo.go (1)
  • MessageRepository (9-13)
ZaeXT/internal/repository/user_repo.go (1)
  • UserRepository (9-14)
ZaeXT/internal/repository/category_repo.go (1)
  • CategoryRepository (9-15)
ZaeXT/internal/model/message.go (1)
  • Message (3-10)
ZaeXT/internal/repository/repository.go (4)
ZaeXT/internal/repository/user_repo.go (2)
  • UserRepository (9-14)
  • NewUserRepository (20-22)
ZaeXT/internal/repository/conversation_repo.go (2)
  • ConversationRepository (10-20)
  • NewConversationRepository (26-28)
ZaeXT/internal/repository/message_repo.go (2)
  • MessageRepository (9-13)
  • NewMessageRepository (19-21)
ZaeXT/internal/repository/category_repo.go (2)
  • CategoryRepository (9-15)
  • NewCategoryRepository (21-23)
ZaeXT/internal/repository/db/init.go (5)
ZaeXT/internal/configs/config.go (3)
  • Init (62-83)
  • Conf (10-10)
  • Config (12-19)
ZaeXT/internal/model/user.go (1)
  • User (3-12)
ZaeXT/internal/model/conversation.go (1)
  • Conversation (5-16)
ZaeXT/internal/model/message.go (1)
  • Message (3-10)
ZaeXT/internal/model/category.go (1)
  • Category (3-11)
ZaeXT/internal/configs/config.go (1)
ZaeXT/internal/repository/db/init.go (1)
  • Init (16-69)
ZaeXT/internal/repository/conversation_repo.go (2)
ZaeXT/internal/model/conversation.go (1)
  • Conversation (5-16)
ZaeXT/internal/model/message.go (1)
  • Message (3-10)
ZaeXT/test-scripts/test_cascade_delete.py (3)
ZaeXT/test-scripts/test_backend.py (5)
  • random_string (19-21)
  • print_test_header (23-26)
  • print_success (28-29)
  • print_fail (31-39)
  • get_auth_headers (41-45)
ZaeXT/test-scripts/test_failures.py (4)
  • random_string (16-18)
  • print_test_header (20-23)
  • print_success (25-26)
  • print_fail (28-35)
ZaeXT/test-scripts/test_premissions.py (6)
  • random_string (10-11)
  • print_test_header (13-16)
  • print_success (18-19)
  • print_fail (21-28)
  • get_auth_headers (30-31)
  • register_and_login (33-45)
ZaeXT/test-scripts/test_backend.py (3)
ZaeXT/test-scripts/test_cascade_delete.py (5)
  • random_string (10-11)
  • print_test_header (13-16)
  • print_success (18-19)
  • print_fail (21-28)
  • get_auth_headers (30-31)
ZaeXT/test-scripts/test_failures.py (4)
  • random_string (16-18)
  • print_test_header (20-23)
  • print_success (25-26)
  • print_fail (28-35)
ZaeXT/test-scripts/test_premissions.py (5)
  • random_string (10-11)
  • print_test_header (13-16)
  • print_success (18-19)
  • print_fail (21-28)
  • get_auth_headers (30-31)
ZaeXT/internal/service/user_service.go (8)
ZaeXT/internal/model/user.go (1)
  • User (3-12)
ZaeXT/internal/handler/request/user.go (1)
  • UpdateUserMemory (13-15)
ZaeXT/internal/repository/user_repo.go (1)
  • UserRepository (9-14)
ZaeXT/internal/repository/category_repo.go (1)
  • CategoryRepository (9-15)
ZaeXT/internal/pkg/jwt/jwt.go (2)
  • JWT (17-19)
  • NewJWT (28-32)
ZaeXT/internal/configs/config.go (1)
  • Conf (10-10)
ZaeXT/internal/pkg/hash/bcrypt.go (2)
  • HashPassword (5-8)
  • CheckPasswordHash (10-13)
ZaeXT/internal/model/category.go (1)
  • Category (3-11)
ZaeXT/internal/handler/chat_handler.go (8)
ZaeXT/internal/service/chat_service.go (1)
  • ChatService (18-27)
ZaeXT/internal/handler/response/response.go (2)
  • Fail (28-38)
  • Success (20-26)
ZaeXT/internal/pkg/e/code.go (5)
  • InvalidParams (6-6)
  • Error (5-5)
  • Success (4-4)
  • PermissionDenied (8-8)
  • NotFound (9-9)
ZaeXT/internal/handler/request/chat.go (3)
  • CreateConversation (8-11)
  • UpdateTitle (13-15)
  • ChatMessage (3-6)
ZaeXT/internal/handler/response/chat.go (1)
  • ConversationInfo (5-13)
ZaeXT/internal/model/message.go (1)
  • Message (3-10)
ZaeXT/internal/pkg/streaming/sse.go (1)
  • SSEEvent (8-13)
ZaeXT/internal/model/conversation.go (1)
  • Conversation (5-16)
ZaeXT/test-scripts/test_premissions.py (2)
ZaeXT/test-scripts/test_backend.py (1)
  • get_auth_headers (41-45)
ZaeXT/test-scripts/test_cascade_delete.py (2)
  • get_auth_headers (30-31)
  • register_and_login (33-37)
ZaeXT/internal/model/category.go (2)
ZaeXT/internal/model/base.go (1)
  • BaseModel (5-9)
ZaeXT/internal/model/user.go (1)
  • User (3-12)
🪛 LanguageTool
ZaeXT/README.md

[uncategorized] ~114-~114: 动词的修饰一般为‘形容词(副词)+地+动词’。您的意思是否是:清晰"地"分层
Context: ...管业务规则,而不知道数据具体是如何存储的。 - 可测试性: 这种清晰的分层使得每一层都可以被独立测试,极大地提高了代码质量和可维护性。 --- ...

(wb4)

🪛 markdownlint-cli2 (0.18.1)
ZaeXT/README.md

6-6: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


7-7: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


9-9: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


10-10: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


11-11: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


13-13: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


14-14: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


16-16: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


17-17: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


18-18: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


20-20: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


21-21: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


23-23: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


24-24: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


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

(MD040, fenced-code-language)


129-129: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


130-130: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


131-131: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


133-133: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


134-134: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


135-135: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


142-142: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


143-143: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


145-145: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


146-146: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


147-147: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


154-154: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


155-155: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


162-162: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


163-163: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


164-164: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


166-166: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


167-167: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


169-169: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


170-170: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


171-171: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


173-173: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


174-174: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


175-175: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


177-177: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


178-178: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


180-180: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


181-181: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


188-188: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


189-189: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


190-190: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


192-192: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


193-193: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


195-195: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


196-196: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


197-197: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


199-199: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


200-200: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


207-207: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


208-208: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


210-210: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


211-211: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


213-213: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


214-214: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

🪛 Ruff (0.14.1)
ZaeXT/test-scripts/test_failures.py

17-17: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


18-18: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


42-42: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


52-52: Possible hardcoded password assigned to: "password"

(S105)


56-56: Probable use of requests call without timeout

(S113)


58-58: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


61-61: Probable use of requests call without timeout

(S113)


63-63: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)


71-71: Probable use of requests call without timeout

(S113)


80-80: Possible hardcoded password assigned to: "password"

(S105)


81-81: Probable use of requests call without timeout

(S113)


85-85: Probable use of requests call without timeout

(S113)


90-90: Probable use of requests call without timeout

(S113)


95-95: Probable use of requests call without timeout

(S113)


101-101: Probable use of requests call without timeout

(S113)


108-108: Comment contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF003)


111-111: Possible hardcoded password assigned to: "password"

(S105)


113-113: Comment contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF003)


114-114: Probable use of requests call without timeout

(S113)


116-116: f-string without any placeholders

Remove extraneous f prefix

(F541)


118-118: Probable use of requests call without timeout

(S113)


120-120: f-string without any placeholders

Remove extraneous f prefix

(F541)


123-123: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)


124-124: Probable use of requests call without timeout

(S113)


125-125: Comment contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF003)


127-127: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


131-131: Probable use of requests call without timeout

(S113)


133-133: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


140-140: Probable use of requests call without timeout

(S113)


142-142: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


149-149: Probable use of requests call without timeout

(S113)


158-158: Probable use of requests call without timeout

(S113)


166-166: Probable use of requests call without timeout

(S113)


180-180: String contains ambiguous (FULLWIDTH EXCLAMATION MARK). Did you mean ! (EXCLAMATION MARK)?

(RUF001)


182-182: Do not catch blind exception: Exception

(BLE001)

ZaeXT/test-scripts/test_cascade_delete.py

11-11: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


35-35: Probable use of requests call without timeout

(S113)


36-36: Probable use of requests call without timeout

(S113)


47-47: Possible hardcoded password assigned to: "password"

(S105)


54-54: Probable use of requests call without timeout

(S113)


58-58: Probable use of requests call without timeout

(S113)


61-61: Probable use of requests call without timeout

(S113)


64-64: Probable use of requests call without timeout

(S113)


71-71: Probable use of requests call without timeout

(S113)


75-75: Probable use of requests call without timeout

(S113)


83-83: Probable use of requests call without timeout

(S113)


91-91: Probable use of requests call without timeout

(S113)


100-100: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


103-103: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


108-108: Probable use of requests call without timeout

(S113)


129-129: String contains ambiguous (FULLWIDTH EXCLAMATION MARK). Did you mean ! (EXCLAMATION MARK)?

(RUF001)

ZaeXT/test-scripts/test_backend.py

11-11: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)


20-20: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


21-21: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


38-38: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)


44-44: Avoid specifying long messages outside the exception class

(TRY003)


44-44: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


56-56: Possible hardcoded password assigned to: "password"

(S105)


60-60: Probable use of requests call without timeout

(S113)


68-68: Probable use of requests call without timeout

(S113)


71-71: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


77-77: Probable use of requests call without timeout

(S113)


80-80: f-string without any placeholders

Remove extraneous f prefix

(F541)


86-86: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


87-87: Probable use of requests call without timeout

(S113)


95-95: Probable use of requests call without timeout

(S113)


108-108: Probable use of requests call without timeout

(S113)


119-119: Probable use of requests call without timeout

(S113)


134-134: Probable use of requests call without timeout

(S113)


144-144: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


144-144: String contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF001)


144-144: String contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF001)


149-149: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)


150-150: Probable use of requests call without timeout

(S113)


166-166: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


170-170: Do not catch blind exception: Exception

(BLE001)


176-176: Probable use of requests call without timeout

(S113)


191-191: Probable use of requests call without timeout

(S113)


199-199: Probable use of requests call without timeout

(S113)


208-208: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


212-212: Probable use of requests call without timeout

(S113)


224-224: Probable use of requests call without timeout

(S113)


237-237: Probable use of requests call without timeout

(S113)


245-245: Probable use of requests call without timeout

(S113)


250-250: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


252-252: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)


254-254: Probable use of requests call without timeout

(S113)


258-258: Probable use of requests call without timeout

(S113)


266-266: Probable use of requests call without timeout

(S113)


269-269: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


271-271: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


282-282: String contains ambiguous (FULLWIDTH EXCLAMATION MARK). Did you mean ! (EXCLAMATION MARK)?

(RUF001)


284-284: Do not catch blind exception: Exception

(BLE001)

ZaeXT/test-scripts/test_premissions.py

11-11: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


34-34: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


36-36: Probable use of requests call without timeout

(S113)


41-41: Probable use of requests call without timeout

(S113)


53-53: Possible hardcoded password assigned to: "password"

(S105)


66-66: Probable use of requests call without timeout

(S113)


78-78: Local variable free_model_id is assigned to but never used

Remove assignment to unused variable free_model_id

(F841)


81-81: Probable use of requests call without timeout

(S113)


99-99: Probable use of requests call without timeout

(S113)


102-102: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)


108-108: Probable use of requests call without timeout

(S113)


122-122: Probable use of requests call without timeout

(S113)


130-130: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)


131-131: Probable use of requests call without timeout

(S113)


146-146: String contains ambiguous (FULLWIDTH EXCLAMATION MARK). Did you mean ! (EXCLAMATION MARK)?

(RUF001)

🔇 Additional comments (30)
ZaeXT/README.md (1)

1-237: 📋 文档内容评价

整体上,该 README 提供了全面、清晰的项目概述:

  • ✅ 核心功能描述完整,涵盖认证、对话、权限管理、数据管理等重要模块
  • ✅ 技术栈明确,便于开发者快速了解项目依赖
  • ✅ 分层架构说明清晰,代码关注点分离的解释有助于理解项目设计
  • ✅ API 文档结构化良好,涵盖所有主要端点
  • ✅ 快速开始部分步骤明确,配置说明详细
  • ✅ 测试部分提供了多种测试脚本的运行指导

建议:确保配置文件示例(configs/config.yaml.example)与文档中提到的配置选项保持同步。

ZaeXT/go.mod (2)

3-3: Go版本选择适当。

Go 1.25.2版本于2025年10月7日发布,包含安全修复,是一个稳定、经过良好测试的版本选择。


5-11: 所有直接依赖项均被正确使用,无需调整。

验证结果显示 ZaeXT/go.mod 中列出的五个直接依赖项均已在代码中导入并使用:

  • JWT:ZaeXT/internal/pkg/jwt/jwt.go
  • Viper:ZaeXT/internal/configs/config.go
  • Crypto:ZaeXT/internal/pkg/hash/bcrypt.go
  • GORM 及 MySQL 驱动:ZaeXT/internal/repository/db/init.go 及相关数据访问层文件

依赖项选择与实际使用情况一致。

ZaeXT/internal/pkg/e/code.go (1)

3-31: 代码实现清晰,消息映射设计合理。

状态码定义和消息映射的实现非常规范,GetMsg 函数的回退机制确保了在未找到映射时返回通用错误消息,避免了空字符串的问题。

ZaeXT/test-scripts/test_cascade_delete.py (1)

41-123: 测试逻辑设计完善,验证步骤全面。

级联删除的测试覆盖了关键场景:

  • 创建多级分类结构并验证删除传播
  • 验证关联对话的 category_id 正确被设为 null
  • 确保不相关的分类和对话不受影响

测试的三阶段结构(准备、执行、验证)清晰且易于维护。

ZaeXT/internal/pkg/hash/bcrypt.go (1)

5-13: 密码哈希实现正确且安全。

使用 bcrypt 的默认成本因子(10)在安全性和性能之间取得了良好平衡。函数实现简洁且遵循了最佳实践:

  • HashPassword 正确传播错误
  • CheckPasswordHash 将验证结果简化为布尔值,便于调用方使用
ZaeXT/internal/handler/response/chat.go (1)

5-13: 响应 DTO 设计合理,字段定义准确。

ConversationInfo 结构体设计良好:

  • 使用可空指针 CategoryIDDeletedAt 处理可选字段
  • omitempty 标签确保空值不会出现在 JSON 响应中
  • 支持软删除模式(DeletedAt 字段)
ZaeXT/internal/model/base.go (1)

5-9: 基础模型设计规范,符合 GORM 最佳实践。

BaseModel 提供了标准的 ORM 基础字段,GORM 标签配置正确:

  • primaryKey 将 ID 设置为主键
  • autoCreateTimeautoUpdateTime 实现自动时间戳管理

这种设计可以被其他模型嵌入,减少代码重复。

ZaeXT/configs/config.yaml.example (2)

9-9: 敏感数据占位符设置得当。

配置文件正确使用了占位符来标识需要替换的敏感信息:

  • 数据库密码
  • JWT 密钥
  • API 密钥

这些占位符清楚地提醒部署人员需要替换为实际的生产值。

建议在项目的 README 或部署文档中添加说明,强调:

  1. 必须将所有占位符替换为实际值
  2. JWT 密钥应使用强随机字符串(建议至少 32 字符)
  3. 不要将包含真实凭据的 config.yaml 提交到版本控制系统

Also applies to: 15-15, 19-19


1-40: 配置结构完整且组织良好。

配置文件涵盖了所有必要的方面:

  • 服务器和数据库设置
  • JWT 认证配置
  • AI 模型分层访问控制(free/premium/pro)
  • 日志和回收站策略

注释清晰,为部署人员提供了良好的指导。

ZaeXT/internal/handler/response/category.go (1)

3-8: 分类响应 DTO 设计优秀,支持层级结构。

CategoryInfo 的递归结构设计巧妙:

  • Children 字段使用指针切片,实现了树形层级结构
  • ParentIDChildren 都使用 omitempty,避免在响应中出现不必要的空值
  • 结构支持任意深度的分类嵌套

这种设计非常适合表示和传输分类树。

ZaeXT/internal/handler/middleware/auth.go (1)

13-42: 认证中间件实现正确

JWT 认证中间件的实现是正确的:

  • JWT helper 在中间件注册时创建一次并被闭包捕获,而非每次请求都创建
  • Bearer token 格式验证完整
  • 错误处理恰当,返回统一的响应格式
  • 成功验证后将用户信息存入上下文供后续处理器使用
ZaeXT/internal/handler/response/user.go (1)

5-11: 响应 DTO 结构清晰

UserProfile 响应结构定义简洁明确,JSON 标签使用规范,字段选择合理,没有暴露敏感信息(如密码哈希)。

ZaeXT/internal/service/service.go (1)

7-22: 服务层组装设计良好

服务聚合器的设计采用了清晰的依赖注入模式:

  • 各个子服务通过构造函数正确接收所需的仓储层依赖
  • ChatService 获得了所有必要的仓储和 AI 适配器
  • 结构清晰,便于测试和维护
ZaeXT/internal/model/user.go (1)

3-12: 模型定义良好!

User 模型的结构设计合理,包含了必要的约束和关系:

  • 用户名唯一性约束和长度限制
  • 密码哈希的非空约束
  • 分层权限系统(Tier)带有默认值
  • 与会话和分类的外键关系定义正确
ZaeXT/internal/handler/request/chat.go (3)

3-6: 请求结构设计合理!

ChatMessage 的验证标签设置恰当,消息长度限制(5000字符)合理,ModelID 为可选字段符合实际使用场景。


8-11: LGTM!

CreateConversation 结构设计简洁,使用指针类型表示可选的 CategoryID 是惯用做法。


13-15: 验证规则与数据库约束一致!

UpdateTitle 的标题长度限制(255字符)与数据库模型中的定义保持一致,设计合理。

ZaeXT/internal/handler/request/user.go (2)

8-11: LGTM!

登录请求结构设计合理,仅要求必填字段,无需额外验证。


13-15: 客户端限制合理!

对 MemoryInfo 设置 5000 字符的上限是合理的防护措施,即使数据库字段类型为 text(无限制)。

ZaeXT/internal/handler/router.go (1)

25-28: 依赖注入实现良好!

处理器通过构造函数接收服务依赖,这是清晰的依赖注入模式。

ZaeXT/internal/repository/repository.go (1)

5-19: 仓储层聚合设计优秀!

使用聚合器模式将多个仓储统一管理,所有子仓储共享同一个数据库连接实例,这有利于:

  • 跨仓储的事务管理
  • 依赖注入的简化
  • 代码结构的清晰性
ZaeXT/internal/model/conversation.go (1)

5-16: 会话模型设计周到!

Conversation 模型包含了多个精心设计的特性:

  • DeletedAt 软删除支持,完美配合回收站功能
  • IsTitleUserModified 标志可区分自动生成和用户修改的标题
  • IsTemporary 支持临时会话场景
  • 索引策略合理(UserID, CategoryID, DeletedAt)

这些设计考虑了实际业务需求,体现了良好的领域建模能力。

ZaeXT/internal/handler/category_handler.go (1)

98-112: 递归转换实现正确!

transformCategoriesToDTO 函数正确处理了 nil 值和递归子分类转换,实现了分层结构的 DTO 映射。

ZaeXT/internal/handler/response/response.go (3)

10-14: 响应结构设计标准!

使用统一的响应信封模式,便于前端统一处理 API 响应。


16-38: 响应辅助函数实现清晰!

Success 和 Fail 函数提供了统一的成功/失败响应处理:

  • 自动填充默认消息
  • 正确映射 HTTP 状态码
  • 代码简洁易维护

40-60: 错误码映射完善!

MapErrorCodeToHTTPStatus 涵盖了常见的 HTTP 状态码,并安全地将未知错误默认为 500,设计合理。

ZaeXT/internal/handler/recycle_bin_handler.go (2)

31-46: 列表映射实现清晰,字段处理正确

DeletedAt 判空转指针、CategoryID 透传均合理,返回结构与 response.ConversationInfo 对齐。


3-12: 导入路径与 go.mod 一致,无需修改

验证完成:go.mod 声明的模块为 ai-qa-backend,recycle_bin_handler.go 中的所有导入均正确使用 ai-qa-backend/... 前缀。两者完全一致,编译和运行不会出现问题。此文件的导入配置正确。

Likely an incorrect or invalid review comment.

ZaeXT/internal/service/category_service.go (1)

33-35: 列表方法语义明确

返回按用户筛选的分类集合,和仓储 ListByUserID 对齐。

是否预期仅返回顶层分类(parent_id IS NULL)及其一层子节点?需与前端约定。

Comment on lines +68 to +72
sqlDB, err := gormDB.DB()
if err == nil {
sqlDB.Close()
log.Println("Database connection closed.")
}
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 | 🟡 Minor

检查数据库关闭错误

sqlDB.Close() 的返回值应该被检查。关闭连接时可能会失败,忽略错误会导致资源泄漏问题难以诊断。

应用此 diff 来检查关闭错误:

 	sqlDB, err := gormDB.DB()
 	if err == nil {
-		sqlDB.Close()
+		if err := sqlDB.Close(); err != nil {
+			log.Printf("Error closing database connection: %v", err)
+		}
 		log.Println("Database connection closed.")
 	}
📝 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
sqlDB, err := gormDB.DB()
if err == nil {
sqlDB.Close()
log.Println("Database connection closed.")
}
sqlDB, err := gormDB.DB()
if err == nil {
if err := sqlDB.Close(); err != nil {
log.Printf("Error closing database connection: %v", err)
}
log.Println("Database connection closed.")
}
🤖 Prompt for AI Agents
In ZaeXT/cmd/server/main.go around lines 68 to 72, the call to sqlDB.Close()
ignores its returned error; update the code to capture the error returned by
Close(), check if it is non-nil, and log or handle it (e.g., log.Printf("error
closing DB: %v", err)) so connection close failures are not silently ignored;
retain the existing check that gormDB.DB() succeeded before calling Close().

Comment on lines +175 to +181
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
line := scanner.Text()
if line == "" {
continue
}

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

SSE 解析存在 64KB 行上限风险,需提升 Scanner 缓冲

bufio.Scanner 默认 token 限制 64KB,长 delta 会触发 ErrTooLong,导致流中断。建议扩大缓冲或改用 bufio.Reader

-		scanner := bufio.NewScanner(resp.Body)
+		scanner := bufio.NewScanner(resp.Body)
+		// 提升单行最大尺寸,避免长 delta 触发 ErrTooLong
+		buf := make([]byte, 64*1024)
+		scanner.Buffer(buf, 2*1024*1024)
@@
-		if err := scanner.Err(); err != nil {
+		if err := scanner.Err(); err != nil {
 			errChan <- fmt.Errorf("error reading stream response: %w", err)
 		}

Also applies to: 199-201

🤖 Prompt for AI Agents
In ZaeXT/internal/adapter/volcengine/doubao_client.go around lines 175-181 and
also lines 199-201, the code uses bufio.NewScanner which has a 64KB token limit
and can fail with ErrTooLong for long SSE delta lines; update the SSE parsing to
either replace Scanner with a bufio.Reader-based line reader
(ReadString/ReadBytes) to handle arbitrarily long lines, or keep Scanner but
call scanner.Buffer with a larger buffer and max token size (e.g., provide a
larger initial buffer and a sufficiently large max token size) before scanning;
ensure you handle scanner.Err() and ErrTooLong properly and test with long SSE
payloads.

Comment on lines +75 to +81
if err := viper.Unmarshal(&Conf); err != nil {
return fmt.Errorf("unable to decode config into struct: %w", err)
}

if Conf.JWT.Secret == "" || Conf.VolcEngine.APIKey == "" {
return fmt.Errorf("JWT secret 或 VolcEngine API key 未设置")
}
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

致命:viper.Unmarshal 目标类型错误(传入的是 **Config),会导致 Conf 为空或解码失败

应解码到临时变量(*Config),再赋给全局 Conf;否则后续访问 Conf.JWT 会 panic。

- if err := viper.Unmarshal(&Conf); err != nil {
+ var cfg Config
+ if err := viper.Unmarshal(&cfg); err != nil {
     return fmt.Errorf("unable to decode config into struct: %w", err)
   }
+ Conf = &cfg

加分项(可选):

  • 设置默认值(RetentionDays 等),并校验 >0。
  • 支持环境变量覆盖:viper.AutomaticEnv() + EnvKeyReplacer。
🤖 Prompt for AI Agents
In ZaeXT/internal/configs/config.go around lines 75 to 81, viper.Unmarshal is
currently decoding directly into the global Conf using the wrong pointer level
(passing **Config), which can leave Conf nil and later cause panics when
accessing Conf.JWT; instead, Unmarshal into a local variable of type Config (or
*Config), check the error, then assign the populated value to the global Conf
variable; after assignment validate required fields (e.g., Conf.JWT.Secret and
Conf.VolcEngine.APIKey) and set/validate defaults such as RetentionDays > 0;
optionally enable environment variable overrides with viper.AutomaticEnv() and
viper.SetEnvKeyReplacer before unmarshalling.

Comment on lines +26 to +36
userID, _ := c.Get("userID")
conv, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
response.Fail(c, e.InvalidParams, "无效的对话ID")
return
}

if err := h.chatService.AutoClassify(uint(conv), userID.(uint)); err != nil {
response.Fail(c, e.Error, err.Error())
return
}
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

统一校验 userID/userTier 的获取与断言,避免 panic

多处直接断言 (uint)/(string)。建议抽出辅助函数或就地校验,失效返回 401。

- userID, _ := c.Get("userID")
+ userIDVal, ok := c.Get("userID")
+ if !ok {
+   response.Fail(c, e.Unauthorized, "未认证")
+   return
+ }
+ userID, ok := userIDVal.(uint)
+ if !ok {
+   response.Fail(c, e.Unauthorized, "认证信息无效")
+   return
+ }

ProcessMessage 还需对 userTier 做同样处理。

Also applies to: 41-51, 66-76, 80-100, 102-116, 118-129, 199-205

🤖 Prompt for AI Agents
In ZaeXT/internal/handler/chat_handler.go around lines 26 to 36 (and also apply
same changes at 41-51, 66-76, 80-100, 102-116, 118-129, 199-205), the code
directly retrieves c.Get("userID") (and elsewhere userTier) and type-asserts to
uint/string which can panic; replace these direct assertions with a small helper
(or inline checks) that reads c.Get, verifies presence and correct type, returns
an HTTP 401 Unauthorized using response.Fail when missing or wrong type, and use
the validated values thereafter; apply the same pattern to ProcessMessage for
userTier so all userID/userTier accesses are consistently validated before use.

Comment on lines +62 to +67
# 预期服务器会返回一个通用错误 (500) 或一个更具体的冲突错误 (409)
# 根据我们的 handler 实现,返回的是 500 + 错误信息
if res_second.status_code == 500 and "already exists" in res_second.json().get("msg", ""):
print_success("重复注册 - 服务器按预期拒绝了请求")
else:
print_fail("重复注册 - 服务器未按预期响应", res_second)
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

重复注册不应以 500 作为预期,请以 409/400 为主

测试用例将 500 + “already exists” 作为成功条件,会固化不恰当的服务端语义。建议优先断言 409 Conflict(或 400),若仍返回 500,应将其视为后端待修复问题而非用例成功。示例调整:

-# 预期服务器会返回一个通用错误 (500) 或一个更具体的冲突错误 (409)
-# 根据我们的 handler 实现,返回的是 500 + 错误信息
-if res_second.status_code == 500 and "already exists" in res_second.json().get("msg", ""):
-    print_success("重复注册 - 服务器按预期拒绝了请求")
-else:
-    print_fail("重复注册 - 服务器未按预期响应", res_second)
+acceptable = {409, 400}
+if res_second.status_code in acceptable:
+    print_success("重复注册 - 服务器按预期拒绝了请求")
+else:
+    print_fail("重复注册 - 非预期状态码(理应为 409/400)", res_second)
📝 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
# 预期服务器会返回一个通用错误 (500) 或一个更具体的冲突错误 (409)
# 根据我们的 handler 实现,返回的是 500 + 错误信息
if res_second.status_code == 500 and "already exists" in res_second.json().get("msg", ""):
print_success("重复注册 - 服务器按预期拒绝了请求")
else:
print_fail("重复注册 - 服务器未按预期响应", res_second)
acceptable = {409, 400}
if res_second.status_code in acceptable:
print_success("重复注册 - 服务器按预期拒绝了请求")
else:
print_fail("重复注册 - 非预期状态码(理应为 409/400)", res_second)
🧰 Tools
🪛 Ruff (0.14.1)

63-63: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)

🤖 Prompt for AI Agents
In ZaeXT/test-scripts/test_failures.py around lines 62 to 67, the test currently
treats a 500 + "already exists" message as a success for duplicate registration;
change the assertion logic to treat 409 Conflict (preferably) or 400 Bad Request
as the expected successful rejection and treat 500 as a failure. Concretely,
check if res_second.status_code == 409 (or res_second.status_code in (409, 400))
and optionally verify the error message contains "already exists" to
print_success; otherwise call print_fail (including when status_code == 500) so
server-side 500s are reported as backend bugs rather than test successes.

Comment on lines +149 to +154
res_bob_delete = requests.delete(f"{BASE_URL}/conversations/{alice_conv_id}", headers=bob_headers)
if res_bob_delete.status_code == 500 and "permission denied" in res_bob_delete.json().get("msg", ""):
print_success("权限隔离 - Bob 删除 Alice 对话被按预期拒绝")
else:
print_fail("权限隔离 - Bob 删除 Alice 对话的请求未被正确拒绝", res_bob_delete)

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

权限拒绝应断言 403(或 404/401),避免将 500 视为“成功”

删除他人资源属于权限不足语义,应以 403 Forbidden(或隐藏资源时 404/401)为主。建议如下:

-res_bob_delete = r("DELETE", f"/conversations/{alice_conv_id}", headers=bob_headers)
-if res_bob_delete.status_code == 500 and "permission denied" in res_bob_delete.json().get("msg", ""):
-    print_success("权限隔离 - Bob 删除 Alice 对话被按预期拒绝")
-else:
-    print_fail("权限隔离 - Bob 删除 Alice 对话的请求未被正确拒绝", res_bob_delete)
+res_bob_delete = r("DELETE", f"/conversations/{alice_conv_id}", headers=bob_headers)
+if res_bob_delete.status_code in (403, 404, 401):
+    print_success("权限隔离 - Bob 删除 Alice 对话被按预期拒绝")
+else:
+    print_fail("权限隔离 - 非预期状态码(理应为 403/404/401)", res_bob_delete)

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

🧰 Tools
🪛 Ruff (0.14.1)

149-149: Probable use of requests call without timeout

(S113)

🤖 Prompt for AI Agents
In ZaeXT/test-scripts/test_failures.py around lines 149 to 154, the test treats
a 500 + "permission denied" response as the expected behavior when Bob attempts
to delete Alice's conversation; change the assertion to expect a proper
authorization status (prefer 403 Forbidden, or accept 404/401 if your API hides
resources) instead of 500, i.e., check res_bob_delete.status_code in (403,) or
(403,404,401) per API design and update the success/fail messages accordingly
and include the full response object in the failure output for debugging.

Comment on lines +159 to +162
if res_bob_chat.status_code != 200:
print_success("权限隔离 - Bob 向 Alice 对话发消息被按预期拒绝")
else:
print_fail("权限隔离 - Bob 向 Alice 对话发消息的请求未被正确拒绝", res_bob_chat)
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 | 🟡 Minor

对“向他人会话发消息”建议断言 403/404/401,而不是只要非 200 就算通过

当前仅判断“不是 200 即成功”,断言过于宽松。建议改为明确的权限类状态码集合:

-res_bob_chat = r("POST", f"/conversations/{alice_conv_id}/messages", headers=bob_headers, json=chat_payload)
-if res_bob_chat.status_code != 200:
-     print_success("权限隔离 - Bob 向 Alice 对话发消息被按预期拒绝")
-else:
-     print_fail("权限隔离 - Bob 向 Alice 对话发消息的请求未被正确拒绝", res_bob_chat)
+res_bob_chat = r("POST", f"/conversations/{alice_conv_id}/messages", headers=bob_headers, json=chat_payload)
+if res_bob_chat.status_code in (403, 404, 401):
+    print_success("权限隔离 - Bob 向 Alice 对话发消息被按预期拒绝")
+else:
+    print_fail("权限隔离 - 非预期状态码(理应为 403/404/401)", res_bob_chat)
🤖 Prompt for AI Agents
In ZaeXT/test-scripts/test_failures.py around lines 159 to 162, the current
check treats any non-200 response as a successful permission denial; change this
to explicitly assert the response status_code is one of the expected
permission-related codes (e.g., 401, 403, 404). Replace the if-condition to
check membership in the allowed_statuses set/list and update the success/fail
messages accordingly so the test only passes when res_bob_chat.status_code is in
{401, 403, 404} and fails otherwise (include the actual response in the failure
message).

Comment on lines +166 to +171
res_verify = requests.get(f"{BASE_URL}/conversations", headers=alice_headers)
found = any(conv['id'] == alice_conv_id for conv in res_verify.json().get("data", []))
if found:
print_success("权限隔离验证 - Alice 的对话安然无恙")
else:
print_fail("权限隔离验证 - Alice 的对话被错误地删除了")
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 | 🟡 Minor

在解析响应 JSON 之前先断言状态码,避免因 401/5xx 导致的 JSONDecodeError

补充状态码校验,并在失败时输出响应帮助定位问题:

-print("  -> 验证 Alice 的对话仍然存在")
-res_verify = r("GET", "/conversations", headers=alice_headers)
-found = any(conv['id'] == alice_conv_id for conv in res_verify.json().get("data", []))
+print("  -> 验证 Alice 的对话仍然存在")
+res_verify = r("GET", "/conversations", headers=alice_headers)
+assert_status_code(res_verify, 200, "查询会话列表")
+found = any(conv.get('id') == alice_conv_id for conv in res_verify.json().get("data", []))

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

🧰 Tools
🪛 Ruff (0.14.1)

166-166: Probable use of requests call without timeout

(S113)

🤖 Prompt for AI Agents
In ZaeXT/test-scripts/test_failures.py around lines 166–171, the code parses
res_verify.json() without checking the HTTP status, which can raise
JSONDecodeError on 401/5xx; update the snippet to first check
res_verify.status_code (or call res_verify.raise_for_status()) and when the
response is not OK, print or log the status code and response text/body to aid
debugging and exit/fail the test, only then parse the JSON and perform the
existing found check.

Comment on lines +55 to +63
# 1. 创建一个 free 用户和一个 premium 用户
free_user_username = f"free_user_{random_string()}"
premium_user_username = f"premium_user_{random_string()}" # 注意前缀

free_user_token = register_and_login(free_user_username, password)
premium_user_token = register_and_login(premium_user_username, password)

print(f" -> 已成功创建并登录 'free' 用户 ({free_user_username}) 和 'premium' 用户 ({premium_user_username})")

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

测试对“premium”身份的前置假设需验证,否则用例不稳定

脚本假设以 premium_user_ 前缀注册会成为 premium,但 service 注册默认 Tier: "free"。请确认是否存在自动升级逻辑或预置任务。

Also applies to: 85-94


🏁 Script executed:

#!/bin/bash
# 搜索是否存在基于用户名前缀/模式设置 premium 的逻辑或升级接口
rg -nP -C2 '(premium|Tier|userTier|Upgrade|SetTier|AuthorizeModel|ListAvailableModels)' --type go
rg -nP -C2 'premium_user_' 

Length of output: 18311


测试依赖不存在的自动升级逻辑,导致用例始终失败

ZaeXT/internal/service/user_service.go 第 53 行,所有新注册用户的 Tier 都硬编码为 "free"。服务代码中不存在基于用户名前缀自动升级至 premium 的逻辑。因此 premium_user_token 实际获得的仍是 "free" 权限,与测试预期不符。

需要通过以下之一来修复测试:

  • 在服务中实现用户升级接口,测试中调用升级接口提升 premium_user 的权限,或
  • 修改测试假设,改为验证 free 用户的权限限制
🤖 Prompt for AI Agents
In ZaeXT/test-scripts/test_premissions.py around lines 55-63, the test assumes
new users with a "premium_" prefix are auto-upgraded to premium, but in
ZaeXT/internal/service/user_service.go line 53 all new users are created with
tier "free" and no auto-upgrade exists; fix the test by either (A) invoking the
service's upgrade endpoint after registering the premium_user (call the user
upgrade API with premium_user_token, assert the upgrade response succeeded, then
refresh token or fetch user info and assert tier == "premium"), or (B) change
the test expectations to treat both created users as "free" (verify
premium_user_token has free-tier limitations instead of expecting premium
capabilities); implement one of these two flows and remove the incorrect
prefix-based assumption.

[Feat] 增加切换思考模式的功能,更新测试脚本
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: 7

♻️ Duplicate comments (15)
ZaeXT/internal/adapter/volcengine/doubao_client.go (1)

187-213: SSE 解析存在 64KB 行上限风险

与先前评审意见一致:bufio.Scanner 默认的 64KB token 限制可能导致长 delta 触发 ErrTooLong 错误,中断流式响应。

应用此差异扩大缓冲区:

 		scanner := bufio.NewScanner(resp.Body)
+		// 提升单行最大尺寸,避免长 delta 触发 ErrTooLong
+		buf := make([]byte, 64*1024)
+		scanner.Buffer(buf, 2*1024*1024)
 		for scanner.Scan() {
ZaeXT/internal/handler/chat_handler.go (8)

25-39: 重复问题:userID 类型断言仍未验证

与之前评审相同,第 26 行和第 33 行直接对 userID 进行类型断言而未检查其存在性和类型,可能导致 panic。


41-64: 重复问题:userID 类型断言仍未验证

第 42 行和第 50 行存在与 AutoClassify 相同的问题。


66-78: 重复问题:userID 类型断言仍未验证

第 67 行和第 69 行存在相同的类型断言问题。


80-100: 重复问题:userID 类型断言仍未验证

第 81 行和第 94 行存在相同的类型断言问题。


102-116: 重复问题:userID 类型断言仍未验证

第 103 行和第 110 行存在相同的类型断言问题。


124-127: 重复问题:userID 和 userTier 类型断言仍未验证

第 124-127 行对 userIDuserTier 进行了不安全的类型断言,可能导致 panic。


176-176: 重复问题:SSE 错误事件 JSON 拼接仍存在转义风险

第 176 行通过字符串拼接构造 JSON,当错误消息包含引号等特殊字符时会产生无效的 JSON。应使用 json.Marshal 序列化。

需要在文件顶部添加 "encoding/json" 导入,并修改为:

+import (
+	"encoding/json"
+	// ... 其他导入
+)
+
 // ...
-				errorData := `{"message":"` + streamError.Error() + `"}`
-				event := streaming.SSEEvent{Event: "error", Data: errorData}
+				errJSON, _ := json.Marshal(map[string]string{"message": streamError.Error()})
+				event := streaming.SSEEvent{Event: "error", Data: string(errJSON)}

204-211: 重复问题:userTier 类型断言仍未验证

第 205-206 行对 userTier 进行了不安全的类型断言。

ZaeXT/internal/service/chat_service.go (4)

87-87: 重复问题:JSON 编码错误仍被忽略

第 87 行 json.Marshal 的错误被忽略,当分类项包含非法字符时可能导致后续提示内容异常。


108-109: 重复问题:free 模型列表访问未判空

第 108-109 行直接访问 freeModels[0] 而未检查切片长度,当无可用模型时会 panic。


305-306: 重复问题:自动标题生成同样存在模型列表访问未判空

第 305-306 行直接访问 freeModels[0] 而未检查长度,存在 panic 风险。


309-317: 重复问题:自动标题只读取首个流块导致截断

第 309-317 行使用 select 仅消费第一条 chunk,当标题通过多块返回时会被截断。建议聚合完整流后再更新数据库。

应修改为:

-	select {
-	case title := <-resChan:
-		if title != "" {
-			conv.Title = strings.Trim(title, "\""" ")
-			_ = s.convRepo.Update(conv)
-		}
-	case <-errChan:
-
-	}
+	var b strings.Builder
+	for chunk := range resChan {
+		b.WriteString(chunk)
+	}
+	title := strings.Trim(b.String(), "\""" \n\t")
+	if title != "" {
+		conv.Title = title
+		_ = s.convRepo.Update(conv)
+	}
+	// 清理错误通道(若有)
+	select {
+	case <-errChan:
+	default:
+	}
ZaeXT/test-scripts/test_backend.py (2)

6-6: 缺少依赖声明:请在依赖文件中添加 sseclient-py

脚本导入了 from sseclient import SSEClient,但仓库未声明 sseclient-py 依赖。请在 requirements.txtpyproject.toml 中显式加入,否则 CI/新环境会失败。此前已有同类指摘,此处为复提以确保纳入本 PR。

建议新增/更新依赖清单(示例):

requests>=2.32.3
sseclient-py>=1.8.0

可用脚本快速核验是否已声明:

#!/bin/bash
fd -a -t f 'requirements*.txt' 'pyproject.toml' 'setup.py'
rg -nP '^\s*sseclient(-py)?\b' -g 'requirements*.txt' || true
rg -n 'sseclient' pyproject.toml setup.py 2>/dev/null || true

170-172: 避免裸捕获 Exception:仅捕获网络相关异常

这里应收敛为 requests.RequestException,以避免吞掉编程错误并降低可观测性。此前有相同建议,本次为复提并给出具体补丁。

-    except Exception as e:
-        print_fail(f"流式聊天请求失败: {e}")
+    except requests.RequestException as e:
+        print_fail(f"流式聊天请求失败: {e}")
🧹 Nitpick comments (6)
ZaeXT/internal/adapter/volcengine/doubao_client.go (2)

146-151: 简化思维模式配置逻辑

当前的 if-else 结构可以简化,提高代码可读性。

应用此差异简化代码:

-		var thinkingPayload *apiThinking
-		if enableThinking {
-			thinkingPayload = &apiThinking{Type: "enabled"}
-		} else {
-			thinkingPayload = &apiThinking{Type: "disabled"}
-		}
+		thinkingType := "disabled"
+		if enableThinking {
+			thinkingType = "enabled"
+		}
+		thinkingPayload := &apiThinking{Type: thinkingType}

119-130: 权限验证与错误处理设计良好

方法正确使用 goroutine 处理流式响应,通道管理得当(缓冲错误通道防止阻塞),并在开始时验证模型权限。建议增强第 128 行的错误消息,包含具体的模型 ID 和用户层级以便调试。

可选的错误消息改进:

 		if !a.isValidModelForTier(modelID, userTier) {
-			errChan <- errors.New("permission denied for the selected model")
+			errChan <- fmt.Errorf("permission denied: model %s not available for tier %s", modelID, userTier)
 			return
 		}
ZaeXT/test-scripts/test_backend.py (4)

149-156: SSE 请求建议显式声明 Accept: text/event-stream

部分网关/代理在缺少 Accept 头时不会以 SSE 方式回传,导致解析异常或阻塞。

示例:

- response = requests.post(f"{BASE_URL}/conversations/{conversation_id}/messages", 
-                          headers=get_auth_headers(), 
+ response = requests.post(f"{BASE_URL}/conversations/{conversation_id}/messages", 
+                          headers={**get_auth_headers(), "Accept": "text/event-stream"},
                           json=chat_payload, 
                           stream=True,
                           timeout=SSE_TIMEOUT)

其余 SSE 调用同理替换。

Also applies to: 329-335, 363-367, 378-382, 411-417


31-40: 测试失败时建议使用 sys.exit 而非 exit

exit() 面向交互解释器,脚本中应使用 sys.exit() 更稳妥。

@@
-import json
+import json
+import sys
@@
-    exit(1)
+    sys.exit(1)

80-80: 移除无效 f-string 前缀

该行未包含占位符,f 前缀多余。

-        print(f"      默认分类已创建 (下一步验证)")
+        print("      默认分类已创建 (下一步验证)")

8-10: 让 BASE_URL 支持环境变量覆盖,便于 CI/多环境运行

本地/CI/容器环境的后端地址往往不同,建议支持 BASE_URL 从环境读取并提供默认值。

-import requests
+import requests
+import os
@@
-BASE_URL = "http://localhost:8080/api/v1"
+BASE_URL = os.getenv("BASE_URL", "http://localhost:8080/api/v1")
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 223af49 and e8c3e45.

📒 Files selected for processing (6)
  • ZaeXT/internal/adapter/volcengine/doubao_client.go (1 hunks)
  • ZaeXT/internal/handler/chat_handler.go (1 hunks)
  • ZaeXT/internal/handler/request/chat.go (1 hunks)
  • ZaeXT/internal/handler/router.go (1 hunks)
  • ZaeXT/internal/service/chat_service.go (1 hunks)
  • ZaeXT/test-scripts/test_backend.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • ZaeXT/internal/handler/request/chat.go
  • ZaeXT/internal/handler/router.go
🧰 Additional context used
🧬 Code graph analysis (4)
ZaeXT/internal/handler/chat_handler.go (8)
ZaeXT/internal/service/chat_service.go (1)
  • ChatService (19-28)
ZaeXT/internal/handler/response/response.go (2)
  • Fail (28-38)
  • Success (20-26)
ZaeXT/internal/pkg/e/code.go (5)
  • InvalidParams (6-6)
  • Error (5-5)
  • Success (4-4)
  • PermissionDenied (8-8)
  • NotFound (9-9)
ZaeXT/internal/handler/request/chat.go (3)
  • CreateConversation (9-12)
  • UpdateTitle (14-16)
  • ChatMessage (3-7)
ZaeXT/internal/handler/response/chat.go (1)
  • ConversationInfo (5-13)
ZaeXT/internal/model/message.go (1)
  • Message (3-10)
ZaeXT/internal/pkg/streaming/sse.go (1)
  • SSEEvent (8-13)
ZaeXT/internal/model/conversation.go (1)
  • Conversation (5-16)
ZaeXT/internal/adapter/volcengine/doubao_client.go (2)
ZaeXT/internal/model/message.go (1)
  • Message (3-10)
ZaeXT/internal/configs/config.go (1)
  • ModelInfo (41-45)
ZaeXT/test-scripts/test_backend.py (3)
ZaeXT/test-scripts/test_cascade_delete.py (1)
  • get_auth_headers (30-31)
ZaeXT/test-scripts/test_premissions.py (1)
  • get_auth_headers (30-31)
xunzhuXY/ciallo/static/js/chat.js (1)
  • title (153-153)
ZaeXT/internal/service/chat_service.go (7)
ZaeXT/internal/adapter/volcengine/doubao_client.go (2)
  • ChatRequest (45-48)
  • AvailableModel (50-53)
ZaeXT/internal/model/conversation.go (1)
  • Conversation (5-16)
ZaeXT/internal/repository/conversation_repo.go (1)
  • ConversationRepository (10-20)
ZaeXT/internal/repository/message_repo.go (1)
  • MessageRepository (9-13)
ZaeXT/internal/repository/user_repo.go (1)
  • UserRepository (9-14)
ZaeXT/internal/repository/category_repo.go (1)
  • CategoryRepository (9-15)
ZaeXT/internal/model/message.go (1)
  • Message (3-10)
🪛 Ruff (0.14.1)
ZaeXT/test-scripts/test_backend.py

11-11: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)


20-20: Docstring contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF002)


21-21: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


38-38: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)


44-44: Avoid specifying long messages outside the exception class

(TRY003)


44-44: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


56-56: Possible hardcoded password assigned to: "password"

(S105)


60-60: Probable use of requests call without timeout

(S113)


68-68: Probable use of requests call without timeout

(S113)


71-71: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


77-77: Probable use of requests call without timeout

(S113)


80-80: f-string without any placeholders

Remove extraneous f prefix

(F541)


86-86: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


87-87: Probable use of requests call without timeout

(S113)


95-95: Probable use of requests call without timeout

(S113)


108-108: Probable use of requests call without timeout

(S113)


119-119: Probable use of requests call without timeout

(S113)


134-134: Probable use of requests call without timeout

(S113)


144-144: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


149-149: Probable use of requests call without timeout

(S113)


166-166: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


170-170: Do not catch blind exception: Exception

(BLE001)


176-176: Probable use of requests call without timeout

(S113)


193-193: Probable use of requests call without timeout

(S113)


201-201: Probable use of requests call without timeout

(S113)


209-209: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


209-209: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


212-212: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


216-216: Probable use of requests call without timeout

(S113)


224-224: Probable use of requests call without timeout

(S113)


228-228: Comment contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF003)


234-234: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


238-238: Probable use of requests call without timeout

(S113)


250-250: Probable use of requests call without timeout

(S113)


263-263: Probable use of requests call without timeout

(S113)


271-271: Probable use of requests call without timeout

(S113)


276-276: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


278-278: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)


280-280: Probable use of requests call without timeout

(S113)


284-284: Probable use of requests call without timeout

(S113)


292-292: Probable use of requests call without timeout

(S113)


295-295: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


297-297: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


311-311: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


312-312: Probable use of requests call without timeout

(S113)


318-318: Probable use of requests call without timeout

(S113)


324-324: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


324-324: String contains ambiguous (FULLWIDTH QUESTION MARK). Did you mean ? (QUESTION MARK)?

(RUF001)


329-329: Probable use of requests call without timeout

(S113)


344-344: String contains ambiguous (FULLWIDTH EXCLAMATION MARK). Did you mean ! (EXCLAMATION MARK)?

(RUF001)


346-346: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


348-348: Do not catch blind exception: Exception

(BLE001)


356-356: Probable use of requests call without timeout

(S113)


359-359: Comment contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF003)


360-360: String contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF001)


360-360: String contains ambiguous (FULLWIDTH QUESTION MARK). Did you mean ? (QUESTION MARK)?

(RUF001)


361-361: String contains ambiguous (FULLWIDTH QUESTION MARK). Did you mean ? (QUESTION MARK)?

(RUF001)


363-363: Probable use of requests call without timeout

(S113)


370-370: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


371-371: Do not catch blind exception: Exception

(BLE001)


374-374: Comment contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF003)


375-375: String contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF001)


375-375: String contains ambiguous (FULLWIDTH QUESTION MARK). Did you mean ? (QUESTION MARK)?

(RUF001)


376-376: String contains ambiguous (FULLWIDTH QUESTION MARK). Did you mean ? (QUESTION MARK)?

(RUF001)


378-378: Probable use of requests call without timeout

(S113)


386-386: String contains ambiguous (FULLWIDTH EXCLAMATION MARK). Did you mean ! (EXCLAMATION MARK)?

(RUF001)


388-388: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


389-389: Do not catch blind exception: Exception

(BLE001)


397-397: Probable use of requests call without timeout

(S113)


402-402: Comment contains ambiguous (FULLWIDTH COLON). Did you mean : (COLON)?

(RUF003)


402-402: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)


411-411: Probable use of requests call without timeout

(S113)


420-420: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)


423-423: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)


424-424: Comment contains ambiguous (FULLWIDTH LEFT PARENTHESIS). Did you mean ( (LEFT PARENTHESIS)?

(RUF003)


424-424: Comment contains ambiguous (FULLWIDTH RIGHT PARENTHESIS). Did you mean ) (RIGHT PARENTHESIS)?

(RUF003)


425-425: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)


434-434: String contains ambiguous (FULLWIDTH EXCLAMATION MARK). Did you mean ! (EXCLAMATION MARK)?

(RUF001)


436-436: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)


437-437: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


440-440: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


442-442: Do not catch blind exception: Exception

(BLE001)


453-453: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)


456-456: String contains ambiguous (FULLWIDTH EXCLAMATION MARK). Did you mean ! (EXCLAMATION MARK)?

(RUF001)


458-458: Do not catch blind exception: Exception

(BLE001)

🔇 Additional comments (13)
ZaeXT/internal/adapter/volcengine/doubao_client.go (4)

45-61: 公共类型设计合理

类型定义清晰,ChatRequestAvailableModelVolcengineAdapter 的结构符合服务层需求,字段命名和组织良好。


63-77: 验证 3 分钟超时配置是否符合预期

HTTP 客户端超时设置为 3 分钟,对于流式响应较为合理,但建议确认该值是否满足实际业务场景需求。此外,第 65-69 行硬编码了层级映射,未来如需扩展可考虑从配置读取。


79-99: 层级过滤逻辑正确

方法正确实现了基于用户层级的模型过滤,未知层级默认为 0(free)是合理的降级策略。


101-117: 模型权限验证逻辑正确

方法正确验证了用户是否有权限使用指定模型,与 GetAvailableModelsForTier 的逻辑保持一致。

ZaeXT/internal/handler/chat_handler.go (1)

189-202: 实现正确且高效

DTO 转换函数逻辑清晰,预分配切片长度优化了性能。

ZaeXT/internal/service/chat_service.go (7)

144-146: 实现正确

简单的委托调用,逻辑清晰。


148-156: 实现正确

创建对话的逻辑符合标准模式。


158-168: 实现正确

包含必要的权限检查和消息加载逻辑。


170-172: 实现正确

简单的列表查询,无问题。


174-269: 流式处理逻辑改进良好

与之前评审相比,本版本已正确实现:

  • 第 219 行的 defer close(handlerErrChan) 确保通道在 goroutine 退出时关闭
  • 第 203-212 行的模型选择包含了适当的边界检查
  • 错误处理路径统一通过 defer 关闭通道,避免了重复关闭的风险

271-279: 实现正确

标题更新逻辑正确,正确设置了 IsTitleUserModified 标志。


281-288: 实现正确

删除前进行了必要的权限验证。

ZaeXT/test-scripts/test_backend.py (1)

130-146: 在使用 available_models[0]['id'] 前添加防御性校验

若未先执行模型拉取流程或后端返回空列表,这里会抛 IndexError

     print_test_header("核心对话与聊天流程")
+    if not available_models:
+        print_fail("模型列表为空,请先执行 test_models_and_categories_flow()")

同样建议在其他引用处(Lines 324-326, 361-377, 406-408)加相同校验。

Comment on lines +181 to +185
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
errChan <- fmt.Errorf("api request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
return
}
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 | 🟡 Minor

处理错误响应体读取失败

第 182 行忽略了 io.ReadAll 的错误,当读取失败时可能导致错误信息不完整。

应用此差异处理读取错误:

 		if resp.StatusCode != http.StatusOK {
-			bodyBytes, _ := io.ReadAll(resp.Body)
-			errChan <- fmt.Errorf("api request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
+			bodyBytes, readErr := io.ReadAll(resp.Body)
+			if readErr != nil {
+				errChan <- fmt.Errorf("api request failed with status %d (failed to read body: %w)", resp.StatusCode, readErr)
+			} else {
+				errChan <- fmt.Errorf("api request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
+			}
 			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 resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
errChan <- fmt.Errorf("api request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
return
}
if resp.StatusCode != http.StatusOK {
bodyBytes, readErr := io.ReadAll(resp.Body)
if readErr != nil {
errChan <- fmt.Errorf("api request failed with status %d (failed to read body: %w)", resp.StatusCode, readErr)
} else {
errChan <- fmt.Errorf("api request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
}
return
}
🤖 Prompt for AI Agents
In ZaeXT/internal/adapter/volcengine/doubao_client.go around lines 181 to 185,
the call to io.ReadAll ignores its error so an I/O failure can produce
incomplete diagnostics; change the logic to capture the error from io.ReadAll,
and if reading the body fails, send an error on errChan that includes both the
HTTP status code and the read error (and if possible include any partial body
content), otherwise include the successfully read body in the formatted error
message before returning.

Comment on lines 200 to 203
var streamResp apiStreamResponse
if err := json.Unmarshal([]byte(data), &streamResp); err != nil {
continue
}
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

JSON 解析失败应记录或报告错误

当 SSE 数据行的 JSON 解析失败时,当前代码直接 continue 跳过,这会导致格式错误的响应被静默丢弃,增加调试难度。建议至少记录日志或在开发模式下发送到错误通道。

考虑以下改进:

 				var streamResp apiStreamResponse
 				if err := json.Unmarshal([]byte(data), &streamResp); err != nil {
+					// 可选:记录日志或在开发模式下发送错误
+					// log.Printf("failed to unmarshal SSE data: %v, data: %s", err, data)
 					continue
 				}

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

Comment on lines +60 to +61
res = requests.post(f"{BASE_URL}/register", json=user_credentials)
if res.status_code == 200:
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

所有 HTTP 请求缺少超时,存在挂死风险

无超时会在目标服务无响应时无限阻塞(CI 最常见失败源)。为避免挂死与资源占用,请为所有 requests 调用加超时;SSE 流建议更长读超时。

建议在顶部集中定义:

@@
-import requests
+import requests
+HTTP_TIMEOUT = (5, 15)   # 普通请求:连接5s、读取15s
+SSE_TIMEOUT   = (5, 120) # 流式请求:连接5s、读取120s

示例修改(其余调用同理类推):

- res = requests.post(f"{BASE_URL}/register", json=user_credentials)
+ res = requests.post(f"{BASE_URL}/register", json=user_credentials, timeout=HTTP_TIMEOUT)

- response = requests.post(f"{BASE_URL}/conversations/{conversation_id}/messages",
-                          headers=get_auth_headers(),
-                          json=chat_payload,
-                          stream=True)
+ response = requests.post(f"{BASE_URL}/conversations/{conversation_id}/messages",
+                          headers=get_auth_headers(),
+                          json=chat_payload,
+                          stream=True,
+                          timeout=SSE_TIMEOUT)

可选:用 requests.Session + HTTPAdapter 配置重试/backoff,提升网络脆弱场景下的稳定性。

Also applies to: 68-69, 77-78, 87-88, 95-96, 108-110, 119-121, 134-135, 149-156, 176-177, 193-195, 201-202, 216-218, 224-225, 238-239, 250-252, 263-265, 271-274, 280-280, 284-286, 292-293, 312-314, 318-321, 329-335, 356-357, 363-367, 378-382, 397-399, 411-417

🧰 Tools
🪛 Ruff (0.14.1)

60-60: Probable use of requests call without timeout

(S113)

🤖 Prompt for AI Agents
In ZaeXT/test-scripts/test_backend.py around lines 60-61 (and the other listed
locations), all requests calls lack timeouts which can hang CI; define a
centralized timeout variable at the top (e.g., CONNECT_TIMEOUT and READ_TIMEOUT
or a single TIMEOUT tuple) and pass it to every requests call via the timeout
parameter (for SSE/streaming use a larger read timeout), update
requests.post/get/put/delete/... to include timeout=... throughout the file, and
optionally switch to a requests.Session with an HTTPAdapter to configure
retries/backoff for network resilience.

Comment on lines +173 to +188
# 3. 验证AI自动生成标题 (不变)
print(" -> 验证AI自动生成标题 (等待3秒)")
time.sleep(3)
res = requests.get(f"{BASE_URL}/conversations", headers=get_auth_headers())
conv_found = False
for conv in res.json().get("data", []):
if conv["id"] == conversation_id:
conv_found = True
if "New Chat" not in conv["title"]:
print_success(f"AI自动生成标题成功: '{conv['title']}'")
else:
print_fail("AI未能自动生成标题")
break
if not conv_found:
print_fail("验证标题时找不到对话")

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

标题自动生成校验存在逻辑漏洞且缺少状态码检查(易误判通过)

仅检查不包含 "New Chat" 会在默认中文标题为“新对话”时误判为成功;且未校验列表接口的返回状态。

-    res = requests.get(f"{BASE_URL}/conversations", headers=get_auth_headers())
+    res = requests.get(f"{BASE_URL}/conversations", headers=get_auth_headers(), timeout=HTTP_TIMEOUT)
+    if res.status_code != 200:
+        print_fail("获取对话列表失败", res)
@@
-            if "New Chat" not in conv["title"]:
+            default_titles = {"New Chat", "新对话"}
+            if conv.get("title") and conv["title"] not in default_titles:
                 print_success(f"AI自动生成标题成功: '{conv['title']}'")
             else:
                 print_fail("AI未能自动生成标题")

如后端提供“是否自动生成”的标记字段,优先以该字段断言更稳妥。

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

🧰 Tools
🪛 Ruff (0.14.1)

176-176: Probable use of requests call without timeout

(S113)

🤖 Prompt for AI Agents
In ZaeXT/test-scripts/test_backend.py around lines 173 to 188, the
title-auto-generation check neither verifies the HTTP response status nor
reliably detects default titles (e.g., "New Chat" vs Chinese "新对话"), causing
false positives; update the check to first assert res.status_code == 200 and
handle JSON parsing errors, then when locating the conversation prefer an
explicit backend flag (e.g., conv.get("auto_generated") or
conv.get("auto_title")) if available and assert its truthiness; if no flag
exists, compare conv["title"] against a list of known default titles in multiple
locales (e.g., ["New Chat","新对话","Untitled"]) and treat any match as failure,
otherwise pass as success; ensure you also set conv_found correctly and return
failure if the conversation is missing.

Comment on lines +318 to +321
res_mem_conv = requests.post(f"{BASE_URL}/conversations", headers=get_auth_headers(), json={})
memory_conv_id = res_mem_conv.json()["data"]["id"]

# c. 提出一个依赖于记忆信息的问题
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

创建新对话后直接取 ["data"]["id"],缺少返回校验,异常时会 KeyError

请在读取 ID 前先校验状态码和键存在性,并在失败时输出响应辅助排障。

-    res_mem_conv = requests.post(f"{BASE_URL}/conversations", headers=get_auth_headers(), json={})
-    memory_conv_id = res_mem_conv.json()["data"]["id"]
+    res_mem_conv = requests.post(f"{BASE_URL}/conversations", headers=get_auth_headers(), json={}, timeout=HTTP_TIMEOUT)
+    if res_mem_conv.status_code != 200 or "id" not in res_mem_conv.json().get("data", {}):
+        print_fail("创建对话失败(记忆测试)", res_mem_conv)
+    memory_conv_id = res_mem_conv.json()["data"]["id"]
@@
-    res_multi_conv = requests.post(f"{BASE_URL}/conversations", headers=get_auth_headers(), json={})
-    multiturn_conv_id = res_multi_conv.json()["data"]["id"]
+    res_multi_conv = requests.post(f"{BASE_URL}/conversations", headers=get_auth_headers(), json={}, timeout=HTTP_TIMEOUT)
+    if res_multi_conv.status_code != 200 or "id" not in res_multi_conv.json().get("data", {}):
+        print_fail("创建对话失败(多轮测试)", res_multi_conv)
+    multiturn_conv_id = res_multi_conv.json()["data"]["id"]
@@
-    res_think_conv = requests.post(f"{BASE_URL}/conversations", headers=get_auth_headers(), json={})
-    thinking_conv_id = res_think_conv.json()["data"]["id"]
+    res_think_conv = requests.post(f"{BASE_URL}/conversations", headers=get_auth_headers(), json={}, timeout=HTTP_TIMEOUT)
+    if res_think_conv.status_code != 200 or "id" not in res_think_conv.json().get("data", {}):
+        print_fail("创建对话失败(思考模式)", res_think_conv)
+    thinking_conv_id = res_think_conv.json()["data"]["id"]

Also applies to: 356-358, 397-399

🧰 Tools
🪛 Ruff (0.14.1)

318-318: Probable use of requests call without timeout

(S113)

🤖 Prompt for AI Agents
In ZaeXT/test-scripts/test_backend.py around lines 318-321 (and similarly at
356-358 and 397-399), the test directly accesses res.json()["data"]["id"]
without validating the response; add explicit checks: verify res.status_code is
200 (or expected), ensure the parsed JSON contains "data" and that "id" exists
before accessing it, and on any failure log or raise with the full response
status and body to aid debugging; fail the test (raise AssertionError) if checks
don't pass so errors are clear and informative.

Comment on lines +348 to +350
except Exception as e:
print_fail(f"测试用户记忆功能时流式聊天请求失败: {e}")

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

多处裸捕获 Exception;请按场景收敛异常类型

  • 流式/HTTP 调用:requests.RequestException
  • JSON 解析(若单独处理):json.JSONDecodeError
  • 顶层兜底可保留,但请打印堆栈,便于排障。
-    except Exception as e:
-        print_fail(f"测试用户记忆功能时流式聊天请求失败: {e}")
+    except requests.RequestException as e:
+        print_fail(f"测试用户记忆功能时流式聊天请求失败: {e}")
@@
-    except Exception as e:
-        print_fail(f"多轮对话第一轮请求失败: {e}")
+    except requests.RequestException as e:
+        print_fail(f"多轮对话第一轮请求失败: {e}")
@@
-    except Exception as e:
-        print_fail(f"多轮对话第二轮请求失败: {e}")
+    except requests.RequestException as e:
+        print_fail(f"多轮对话第二轮请求失败: {e}")
@@
-    except Exception as e:
-        print_fail(f"测试思考模式时流式聊天请求失败: {e}")
+    except requests.RequestException as e:
+        print_fail(f"测试思考模式时流式聊天请求失败: {e}")
@@
-    except Exception as e:
-        print(f"\n❌  测试过程中出现意外错误: {e}")
+    except Exception as e:
+        import traceback
+        traceback.print_exc()
+        print(f"\n❌  测试过程中出现意外错误: {e}")

Also applies to: 371-373, 389-391, 442-444, 458-459

🧰 Tools
🪛 Ruff (0.14.1)

348-348: Do not catch blind exception: Exception

(BLE001)

test_recycle_bin_flow() # 确保回收站测试在最后,因为它会删除一个重要对话

print("\n" + "="*50)
print(" 🎉 \033[92m所有测试均已成功通过!\03d[0m")
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 | 🟡 Minor

ANSI 转义码拼写错误,导致颜色复位失败

\03d[0m 应为 \033[0m

-        print("  🎉  \033[92m所有测试均已成功通过!\03d[0m")
+        print("  🎉  \033[92m所有测试均已成功通过!\033[0m")
🧰 Tools
🪛 Ruff (0.14.1)

456-456: String contains ambiguous (FULLWIDTH EXCLAMATION MARK). Did you mean ! (EXCLAMATION MARK)?

(RUF001)

🤖 Prompt for AI Agents
In ZaeXT/test-scripts/test_backend.py around line 456, the ANSI reset escape
sequence is misspelled as "\03d[0m" which prevents color from resetting; replace
it with the correct sequence "\033[0m" (or use a double-escaped string
"\\033[0m" if the string is in a context where backslashes are parsed) so the
green color is properly reset after the success message.

ZaeXT added 2 commits October 25, 2025 11:33
[Refactor] 将 AI 流从 string 改为 []byte,统一流数据传递与解析;在 handler 层改用 SSE 原生格式输出;新增会话分类更新与获取消息接口;放宽 UpdateTitle 参数以支持自动生成标题;改进 AutoClassify 和 autoGenerateTitle 的流解析、错误处理与回复聚合保存。
AI总结:
feat(frontend): add complete Vue 3 + Vite frontend scaffold with chat app features

- initialize frontend package (package.json) with dependencies (vue, pinia, vue-router, vue-i18n, tailwind, vitest, etc.) and dev scripts
- add Vite, TypeScript and Tailwind configs (vite.config.ts, tsconfig.*, tailwind.config.ts, postcss.config.js)
- global styles and assets (src/styles/main.css, public vite/vue svg assets)

- App entry & wiring
  - main.ts bootstraps app with pinia, router and i18n
  - App.vue renders RouterView and global widgets (toaster, settings)

- Router
  - router/index.ts with protected routes, guest-only routes, progress bar, and navigation guards

- Internationalization
  - i18n setup (src/i18n) with zh-CN and en-US locale files and persistLocale helper

- API layer
  - axios HTTP client with ApiError handling, interceptors and unified response parsing (src/api/http.ts)
  - REST API modules for auth, categories, conversations, models, recycle-bin, user
  - SSE streaming helper for conversation messages with chunk parsing, heartbeat timeout and abort support (src/api/sse.ts)

- Stores (Pinia)
  - auth store: login/register, token handling, profile fetch, memory update
  - conversation store: list/messages management, streaming flow integration, placeholder assistant messages, aborting streams, conversation CRUD
  - category store: load/add/update/delete categories
  - model store: load models and compute available/locked by user tier
  - recycle-bin store: list/restore/delete
  - central pinia export (src/stores/index.ts)

- Types
  - comprehensive TS types for API, auth, categories, conversations, models, SSE, user, recycle-bin, shims

- Utilities
  - event-bus (mitt) for app events
  - notifications wrapper (emitToast/onToast) + AppToaster component
  - token helpers (get/set/clear and getAuthHeader)
  - JWT decode & expiry check
  - time utilities (dayjs wrappers)

- UI components & features
  - Chat workspace composed of sidebar, header, message list, composer, info panel
  - Reusable UI components: AppEmptyState, AppLoading, AppErrorState, AppMarkdownRenderer (markdown rendering, syntax highlight, copy code), AppSettingsWidget, AppToaster
  - Views: ChatWorkspace, ModelsView, CategoriesView, ProfileView, RecycleBinView, Auth views (Login/Register), ErrorView
  - Features: conversation creation, sending messages, streaming assistant replies, model selection, category management, auto-classify trigger, profile memory editing

- Tests & dev tooling
  - vitest setup (vitest.setup.ts) and unit tests for http client and SSE parser (src/api/__tests__)
  - env types (env.d.ts) and virtual scroller shims

Notes:
- SSE implementation includes parsing JSON chunks in "data:" lines, handling [DONE], heartbeats and user-visible toasts on reconnect/timeouts.
- HTTP client surfaces business errors (payload.code !== 200) as ApiError and centralizes status handling (401/403/404/5xx) with toasts and navigation.
- This commit is a full frontend initial implementation for the AI chat product.
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: 12

♻️ Duplicate comments (5)
ZaeXT/internal/handler/router.go (1)

15-20: CORS 配置在生产环境过于宽松,应改为来源白名单。

AllowAllOrigins: true 存在安全隐患(CSRF/滥用)。请基于环境/配置切换到明确的白名单,并仅在开发模式下放开所有来源。

示例修复(基于环境变量 ALLOWED_ORIGINS,逗号分隔;生产默认不放开):

 import (
   "ai-qa-backend/internal/handler/middleware"
   "ai-qa-backend/internal/service"
   "time"
+  "os"
+  "strings"

   "github.com/gin-contrib/cors"
   "github.com/gin-gonic/gin"
 )
@@
-  config := cors.Config{
-    AllowAllOrigins: true,
-    AllowMethods:    []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
-    AllowHeaders:    []string{"Origin", "Content-Type", "Authorization", "Accept"},
-    ExposeHeaders:   []string{"Content-Length"},
-    MaxAge:          12 * time.Hour,
-  }
+  config := cors.Config{
+    AllowMethods:  []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
+    AllowHeaders:  []string{"Origin", "Content-Type", "Authorization", "Accept", "Last-Event-ID"},
+    ExposeHeaders: []string{"Content-Length"},
+    MaxAge:        12 * time.Hour,
+  }
+  if origins := strings.TrimSpace(os.Getenv("ALLOWED_ORIGINS")); origins != "" {
+    config.AllowOrigins = strings.Split(origins, ",")
+  } else if gin.Mode() == gin.ReleaseMode {
+    // 生产环境请填入实际域名,避免全放开
+    config.AllowOrigins = []string{"https://yourdomain.example"}
+  } else {
+    config.AllowAllOrigins = true
+  }
ZaeXT/internal/adapter/volcengine/doubao_client.go (2)

181-185: 需处理响应体读取错误

io.ReadAll 的错误被忽略,当读取失败时可能导致错误信息不完整。建议捕获该错误并在错误消息中体现。


187-202: Scanner 存在 64KB 行长度限制风险

bufio.Scanner 默认 token 上限为 64KB,当 SSE 数据行过长时会触发 ErrTooLong 导致流中断。建议调用 scanner.Buffer() 扩大缓冲区,或改用 bufio.ReaderReadString/ReadBytes 方法。

ZaeXT/internal/handler/chat_handler.go (1)

26-40: 需统一校验 userID 获取与类型断言

多处直接使用 c.Get("userID") 并进行类型断言 userID.(uint),当值不存在或类型不匹配时会引发 panic。建议提取辅助函数统一校验,失败时返回 401 错误。

应用以下模式修复:

-userID, _ := c.Get("userID")
+userIDVal, ok := c.Get("userID")
+if !ok {
+  response.Fail(c, e.Unauthorized, "未认证")
+  return
+}
+userID, ok := userIDVal.(uint)
+if !ok {
+  response.Fail(c, e.Unauthorized, "认证信息无效")
+  return
+}

此问题同样出现在 UpdateConversationCategoryCreateConversationListConversationsUpdateTitleDeleteConversationListModelsGetMessages 方法中。

ZaeXT/internal/service/chat_service.go (1)

89-89: 不要忽略 JSON 编码错误

json.Marshal 的错误被丢弃,当分类项包含非法字符时可能导致后续提示内容畸形。建议捕获错误并在失败时返回。

🧹 Nitpick comments (46)
ZaeXT/frontend/src/types/api.ts (1)

1-20: 类型定义清晰,结构合理。

API 响应类型定义完整且结构良好:

  • ApiResp<T> 提供了标准的响应包装结构
  • PaginatedResult<T> 适用于分页场景
  • ApiListResp<T> 适用于简单列表响应
  • ApiErrorPayload 专门处理错误情况

类型定义正确,字段命名(如 page_size 使用 snake_case)与后端 API 保持一致。

可选改进:添加 JSDoc 注释以提升开发体验。

建议为每个接口添加 JSDoc 注释,以便在 IDE 中获得更好的类型提示和文档支持:

+/**
+ * 标准 API 响应结构
+ * @template T - 响应数据类型
+ */
 export interface ApiResp<T> {
+  /** 响应状态码 */
   code: number
+  /** 响应消息 */
   msg: string
+  /** 响应数据 */
   data: T
 }

+/**
+ * 分页结果结构
+ * @template T - 列表项类型
+ */
 export interface PaginatedResult<T> {
+  /** 数据项列表 */
   items: T[]
+  /** 总记录数 */
   total: number
+  /** 当前页码 */
   page: number
+  /** 每页大小 */
   page_size: number
 }

+/** 数组类型的 API 响应 */
 export type ApiListResp<T> = ApiResp<T[]>

+/**
+ * API 错误响应载荷
+ */
 export interface ApiErrorPayload {
+  /** 错误代码 */
   code: number
+  /** 错误消息 */
   msg: string
+  /** 额外错误数据(可选)*/
   data?: unknown
 }
ZaeXT/frontend/README.md (1)

51-73: 环境准备与启动步骤明确,建议补充后端健康检查建议。

设置说明清晰易懂。可考虑在第 73 行补充一行建议,提示用户验证后端在 VITE_API_BASE 上的可达性(如 curl 测试 /api/v1/health 或认证端点)。

   Vite 默认监听 `http://localhost:5173`。请确保后端已按 `backend_README.md` 启动,并可通过 `VITE_API_BASE` 访问。
+
+  验证后端连通性:
+  ```bash
+  curl http://localhost:8080/api/v1/health
+  ```
ZaeXT/frontend/.stylelintrc.cjs (1)

1-21: Stylelint 配置完善,支持 Vue 和 Tailwind CSS。

配置优点:

  • 正确配置 Vue 文件的 PostCSS 语法支持
  • 包含 Tailwind CSS 特定的 at-rule 白名单(@tailwind@layer 等)
  • 使用 stylelint-order 插件强制属性排序

可选建议:order/properties-alphabetical-order 规则强制按字母顺序排列 CSS 属性。虽然这能保证一致性,但有些团队更倾向于按逻辑分组(如定位、盒模型、排版等)。如果团队对当前严格的字母排序感到不便,可以考虑调整为逻辑分组。

ZaeXT/frontend/src/env.d.ts (1)

3-3: 建议完善 Vue 模块声明的类型定义

当前的 *.vue 模块声明不完整,缺少导出类型。建议添加完整的类型定义以获得更好的类型安全性。

应用以下改动来完善类型定义:

-declare module '*.vue'
+declare module '*.vue' {
+  import type { DefineComponent } from 'vue'
+  const component: DefineComponent<{}, {}, any>
+  export default component
+}
ZaeXT/internal/handler/request/conversation.go (1)

3-5: 为可选字段补充校验与注释,明确 nil 语义

  • 建议:当提供 category_id 时应为 > 0;未提供表示“取消分类”。可用 Gin/validator 的 tag 约束。
  • 可加注释说明:nil => 移除分类;非 nil => 设置至给定分类 ID。

示例修改:

 type UpdateConversationCategory struct {
-	CategoryID *uint `json:"category_id"`
+	// nil 表示移除分类;非 nil 表示设置到对应分类
+	CategoryID *uint `json:"category_id" binding:"omitempty,gt=0"`
 }

请确认服务层对 nil 的处理是否就是“移除分类”,以避免语义偏差。

ZaeXT/frontend/src/components/AppErrorState.vue (1)

2-12: 将默认插槽改为 i18n,顺带补充无障碍语义

  • 目前默认文案为硬编码中文,不随语言切换。建议使用 $t 作默认内容。
  • 补充 role="alert"aria-live="polite" 以便读屏器正确播报错误状态。

建议修改:

-  <div
-    class="flex flex-col items-center justify-center gap-3 rounded-2xl border border-rose-200/40 bg-rose-500/10 p-6 text-center dark:border-rose-500/30 dark:bg-rose-500/10"
-  >
+  <div
+    class="flex flex-col items-center justify-center gap-3 rounded-2xl border border-rose-200/40 bg-rose-500/10 p-6 text-center dark:border-rose-500/30 dark:bg-rose-500/10"
+    role="alert"
+    aria-live="polite"
+  >
-    <h3 class="text-sm font-semibold text-rose-600 dark:text-rose-300">
-      <slot name="title">发生错误</slot>
+    <h3 class="text-sm font-semibold text-rose-600 dark:text-rose-300" id="app-error-title">
+      <slot name="title">{{$t('common.errorTitle')}}</slot>
     </h3>
-    <p class="max-w-xs text-xs text-rose-500/80 dark:text-rose-200/80">
-      <slot name="description">请稍后重试或联系管理员。</slot>
+    <p class="max-w-xs text-xs text-rose-500/80 dark:text-rose-200/80" aria-describedby="app-error-title">
+      <slot name="description">{{$t('common.errorDescription')}}</slot>
     </p>

同时在各语言包补充 common.errorTitle 与 common.errorDescription 键(见对应评论)。

ZaeXT/internal/handler/response/sse.go (1)

3-13: 根据实际兼容性考虑扩展字段(index、finish_reason、role)

当前仅解析 choices[].delta.content,如果后续需要:

  • 分片顺序/并发聚合:需要 index
  • 结束判断:需要 finish_reason(或单独处理 [DONE]
  • 首片角色:需要 delta.role

可向后兼容地扩展:

 type OpenAIStreamChoiceDelta struct {
-	Content string `json:"content"`
+	Role    *string `json:"role,omitempty"`
+	Content *string `json:"content,omitempty"`
 }
 
 type OpenAIStreamChoice struct {
-	Delta OpenAIStreamChoiceDelta `json:"delta"`
+	Delta         OpenAIStreamChoiceDelta `json:"delta"`
+	Index         *int                    `json:"index,omitempty"`
+	FinishReason  *string                 `json:"finish_reason,omitempty"`
 }
 
 type OpenAIStreamResponse struct {
 	Choices []OpenAIStreamChoice `json:"choices"`
 }

这样既能区分“字段缺省”与“空字符串”,又保留额外信息以便前后端一致化处理。

请确认前端 types/sse.ts 是否也仅使用 content;如已使用 index/finishReason,请一并对齐。

ZaeXT/frontend/tsconfig.node.json (1)

2-23: 配置合理;可按需精简 allowImportingTsExtensions

整体设置与 Vite/Vitest 场景匹配。若没有以 import './x.ts' 的场景,可去掉:

-    "allowImportingTsExtensions": true,

以减少不必要的语法允许范围。

ZaeXT/frontend/src/i18n/locales/zh-CN.json (2)

2-9: 补充通用错误文案键,便于组件复用

为配合 AppErrorState 的 i18n 默认内容,建议在 common 区域新增错误标题与描述:

   "common": {
     "underConstruction": "正在建设中,敬请期待。",
     "backToChat": "返回对话",
     "loading": "加载中...",
     "retry": "重试",
     "confirm": "确认",
-    "cancel": "取消"
+    "cancel": "取消",
+    "errorTitle": "发生错误",
+    "errorDescription": "请稍后重试或联系管理员。"
   },

34-36: 用词微调:将“重生成标题”改为“重新生成标题”

更符合中文习惯:

-    "regenerateTitle": "重生成标题",
+    "regenerateTitle": "重新生成标题",
ZaeXT/frontend/src/components/AppSettingsWidget.vue (3)

55-58: 切换语言时同时更新本地响应式状态,避免依赖副作用

为确保 UI 立即切换,不依赖 persistLocale 内部副作用,直接更新 locale.value

 const switchLocale = () => {
   const next = locale.value === 'zh-CN' ? 'en-US' : 'zh-CN'
-  persistLocale(next)
+  locale.value = next
+  persistLocale(next)
 }

请确认 persistLocale 是否已同步更新 i18n.global.locale.value;若是,两者顺序不限。


28-31: 尊重系统主题偏好作为初始值(提升 UX)

支持 prefers-color-scheme,在无本地存储时遵循系统设置:

 const getInitialTheme = (): 'light' | 'dark' => {
   if (typeof window === 'undefined') return 'dark'
-  return (window.localStorage.getItem('ai-chat-theme') as 'light' | 'dark') ?? 'dark'
+  const saved = window.localStorage.getItem('ai-chat-theme') as 'light' | 'dark' | null
+  if (saved === 'light' || saved === 'dark') return saved
+  return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
 }

3-16: 补充可访问性属性与可复用样式(轻量建议)

  • 为两个切换按钮添加 aria-pressedaria-label,提升可访问性。
  • 抽取重复的按钮类到常量或样式,便于维护(可选)。

示例:

-<button
-  class="rounded-full border border-white/20 bg-slate-900/80 px-3 py-1.5 text-xs text-slate-100 shadow hover:bg-slate-800/80"
-  type="button"
-  @click="toggleTheme"
->
+<button
+  class="rounded-full border border-white/20 bg-slate-900/80 px-3 py-1.5 text-xs text-slate-100 shadow hover:bg-slate-800/80"
+  type="button"
+  :aria-pressed="theme === 'dark'"
+  :aria-label="themeLabel"
+  @click="toggleTheme"
+>
ZaeXT/frontend/src/utils/jwt.ts (1)

8-24: 建议:验证 JWT 结构的完整性

当前实现直接对 token 进行 split 操作,但没有验证 JWT 是否包含三个部分(header.payload.signature)。如果传入的 token 格式不正确,可能会导致意外行为。

建议在解码前添加结构验证:

 export const decodeToken = (token: string): JwtPayload | null => {
   try {
-    const [, payload] = token.split('.')
+    const parts = token.split('.')
+    if (parts.length !== 3) {
+      return null
+    }
+    const payload = parts[1]
     if (!payload) {
       return null
     }
ZaeXT/frontend/src/components/AppMarkdownRenderer.vue (1)

40-66: 建议:使用 i18n 替换硬编码的中文文本

复制按钮的文本("复制" 和 "已复制")是硬编码的中文字符串,这与项目中其他地方使用 i18n 的做法不一致。

建议从 i18n 获取这些文本:

+import { useI18n } from 'vue-i18n'
+
+const { t } = useI18n()
+
 const attachCopyButtons = () => {
   nextTick(() => {
     // ...
-      button.textContent = '复制'
+      button.textContent = t('common.copy')
       button.addEventListener('click', () => {
         // ...
-            button.textContent = '已复制'
+            button.textContent = t('common.copied')
             setTimeout(() => {
-              button.textContent = '复制'
+              button.textContent = t('common.copy')
             }, 2000)

同时需要在语言文件中添加相应的翻译键。

ZaeXT/frontend/src/components/AppLoading.vue (1)

1-16: 建议添加无障碍属性

加载组件的实现简洁有效,但缺少 ARIA 属性以支持屏幕阅读器用户。建议添加 role="status"aria-live="polite" 以提升可访问性。

应用以下改进:

 <template>
-  <div class="flex w-full items-center justify-center py-12 text-center text-sm text-slate-400">
+  <div 
+    role="status" 
+    aria-live="polite"
+    class="flex w-full items-center justify-center py-12 text-center text-sm text-slate-400"
+  >
     <svg class="mr-3 h-5 w-5 animate-spin text-brand" viewBox="0 0 24 24" fill="none">
ZaeXT/frontend/src/views/errors/ErrorView.vue (1)

6-10: 建议将默认文案也使用 i18n 处理以保持一致性。

标题和描述的默认值使用了硬编码的英文文本("Oops"、"Something went wrong."),但按钮文本使用了 i18n。为了保持整体的国际化一致性,建议也将这些默认值改为使用 i18n 键值。

可以考虑这样修改:

 <template>
   <div
     class="flex min-h-screen flex-col items-center justify-center gap-4 bg-surface-light px-6 text-center dark:bg-surface-dark"
   >
     <div class="space-y-2">
-      <h1 class="text-4xl font-semibold text-slate-900 dark:text-slate-100">{{ title }}</h1>
+      <h1 class="text-4xl font-semibold text-slate-900 dark:text-slate-100">{{ title || t('common.errorTitle') }}</h1>
       <p class="max-w-xl text-sm text-slate-500 dark:text-slate-400">
         <slot>
-          {{ description }}
+          {{ description || t('common.errorDescription') }}
         </slot>
       </p>
     </div>

并在 props 默认值中移除硬编码文本:

 withDefaults(
   defineProps<{
     title?: string
     description?: string
   }>(),
   {
-    title: 'Oops',
-    description: 'Something went wrong.',
+    title: '',
+    description: '',
   },
 )
ZaeXT/frontend/src/i18n/locales/en-US.json (1)

61-62: 可以考虑为移动端回收站标签使用不同的文案。

recycleBinShortcutrecycleBinMobile 两个键都使用了相同的文本 "Recycle Bin"。如果它们用于不同的 UI 上下文(桌面端快捷方式和移动端),可以考虑为移动端使用更简短的文案,如 "Bin" 或保持一致。这取决于具体的 UI 设计需求。

ZaeXT/frontend/src/views/recycle-bin/RecycleBinView.vue (1)

94-100: goBack 函数逻辑可以简化。

当前实现检查历史记录长度来决定是回退还是替换路由。可以考虑直接使用 router.back() 并捕获异常,或者简化为始终使用 router.push({ name: 'chat' }),这样逻辑会更清晰。不过当前实现也是可以工作的。

可选的简化方案:

 const goBack = () => {
-  if (window.history.length > 1) {
-    router.back()
-    return
-  }
-  router.replace({ name: 'chat' }).catch(() => undefined)
+  router.push({ name: 'chat' })
 }
ZaeXT/frontend/src/stores/category.ts (2)

8-19: flattenTree 函数可以优化性能。

当前实现使用 shift()unshift() 操作数组,这两个操作的时间复杂度都是 O(n)。对于小规模分类树这不会有问题,但可以改用 pop()push() 来提升性能。

性能优化建议:

 const flattenTree = (nodes: CategoryTreeNode[]): CategoryTreeNode[] => {
   const result: CategoryTreeNode[] = []
   const stack = [...nodes]
   while (stack.length) {
-    const node = stack.shift()!
+    const node = stack.pop()!
     result.push(node)
     if (node.children?.length) {
-      stack.unshift(...node.children)
+      stack.push(...node.children)
     }
   }
   return result
 }

注意:这会改变遍历顺序,如果顺序重要请调整实现。


41-58: Toast 消息可以考虑国际化。

当前 toast 消息是硬编码的中文字符串(lines 43, 50, 56)。虽然对于中文应用可以接受,但如果未来需要支持多语言,建议使用 i18n 的翻译键。

ZaeXT/frontend/src/views/categories/CategoriesView.vue (1)

64-68: 考虑使用模态对话框替代window.prompt。

当前使用 window.prompt 来获取分类名称,这种原生对话框的用户体验不够现代化,且样式无法自定义。建议考虑使用自定义的模态对话框组件(如果项目中已有的话)来提升用户体验。

不过对于当前的功能需求,该实现是可以接受的。

ZaeXT/frontend/src/stores/auth.ts (2)

19-25: 建议清理事件监听器

事件监听器在 Line 25 注册但未在 store 销毁时清理,可能导致潜在的内存泄漏。虽然 Pinia store 是单例,但显式清理仍是最佳实践。

考虑使用 onScopeDispose 清理监听器:

+import { defineStore } from 'pinia'
+import { computed, onScopeDispose, ref } from 'vue'
...
  appEventBus.on('auth:unauthorized', handleUnauthorized)
+  onScopeDispose(() => {
+    appEventBus.off('auth:unauthorized', handleUnauthorized)
+  })

78-85: 建议添加错误处理

updateMemoryInfo 缺少错误处理,与 performLoginperformRegister 的模式不一致。如果 API 调用失败,异常将传播给调用者,可能导致未处理的 Promise rejection。

建议添加 try-catch:

 const updateMemoryInfo = async (payload: UpdateMemoryPayload) => {
   if (!token.value) return
-  await updateMemory(payload)
-  if (profile.value) {
-    profile.value = { ...profile.value, memory_info: payload.memory_info }
+  try {
+    await updateMemory(payload)
+    if (profile.value) {
+      profile.value = { ...profile.value, memory_info: payload.memory_info }
+    }
+    emitToast({ type: 'success', message: '记忆已更新。' })
+  } catch (error) {
+    console.error('[auth] failed to update memory', error)
+    emitToast({ type: 'error', message: '更新记忆失败。' })
   }
-  emitToast({ type: 'success', message: '记忆已更新。' })
 }
ZaeXT/frontend/vite.config.ts (2)

15-19: I18n 配置导致更大的 bundle

runtimeOnly: false 会将 Vue I18n 编译器包含在生产构建中,增加 bundle 大小。如果不需要运行时编译 i18n 消息,建议设置为 true

如果不需要运行时编译,应用此更改:

 VueI18nPlugin({
   include: fileURLToPath(new URL('./src/i18n/locales/**', import.meta.url)),
-  runtimeOnly: false,
+  runtimeOnly: true,
   compositionOnly: true,
 }),

54-56: 测试别名配置重复

Lines 54-56 的测试别名与 Lines 21-34 的 resolve.alias 重复。Vitest 应该自动继承 Vite 的 resolve.alias 配置,这里的重复定义可能是不必要的。

考虑移除重复的别名配置:

   test: {
     environment: 'jsdom',
     globals: true,
     setupFiles: ['./vitest.setup.ts'],
     coverage: {
       provider: 'v8',
       reporter: ['text', 'html', 'lcov'],
       include: ['src/**/*.{ts,vue}'],
       exclude: ['src/**/__tests__/**', 'src/**/__mocks__/**'],
     },
-    alias: {
-      '@': fileURLToPath(new URL('./src', import.meta.url)),
-    },
   },
ZaeXT/frontend/src/features/categories/CategoryNode.vue (1)

54-58: 考虑改进用户体验

使用 window.prompt 收集用户输入的体验较为简陋。对于生产环境,建议使用自定义的模态对话框组件以提供更好的用户体验和样式一致性。

ZaeXT/frontend/src/features/chat/ChatMessageList.vue (1)

85-97: 双 watcher 可能导致冗余调用

Lines 85-90 和 92-97 的两个 watcher 在新消息添加时可能都会触发,导致 scrollToBottom 被调用两次。虽然 nextTick 提供了一定的去抖动效果,但可以考虑合并为单个 watcher:

watch(
  () => [messages.value.length, messages.value.at(-1)?.content],
  () => {
    scrollToBottom()
  },
)

不过当前实现也是可接受的,对性能影响很小。

ZaeXT/frontend/src/router/index.ts (2)

85-95: 统一鉴权状态来源,优先使用 Pinia 中的 token 而非直接读 localStorage。

直接读 localStorage 会与全局状态脱节(如登出时 store 与缓存不同步的时序问题)。建议在守卫内使用 useAuthStore()token 计算登录态;在 SSR 下保留 typeof window 兜底。

-const token = typeof window !== 'undefined' ? window.localStorage.getItem(TOKEN_KEY) : null
+const authStore = useAuthStore()
+const { token } = storeToRefs(authStore)
+const hasToken = !!token.value || (typeof window !== 'undefined' && !!window.localStorage.getItem(TOKEN_KEY))
- if (to.meta.requiresAuth && !token) {
+ if (to.meta.requiresAuth && !hasToken) {
    ...
- if (token && to.meta.guestOnly) {
+ if (hasToken && to.meta.guestOnly) {
    ...
- if (token && to.meta.requiresAuth) {
+ if (hasToken && to.meta.requiresAuth) {
    ...

8-73: 为路由 meta 添加类型增强,避免 any 带来的用法偏差。

requiresAuth/guestOnly 增加 RouteMeta 声明,提升类型安全与可读性。

新增类型声明文件(例如 src/types/router.d.ts):

import 'vue-router'

declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth?: boolean
    guestOnly?: boolean
  }
}
ZaeXT/frontend/src/features/chat/ChatSidebar.vue (3)

55-71: 列表选择未同步到 URL,导致深链/刷新不可复现当前会话。

当前仅调用 selectConversation,不导航到 /chat/:id。建议改为 RouterLink 或在点击时 router.push({ name: 'chat-with-id', params: { id: item.id } }),使选中状态可被地址栏表达并可刷新恢复。

示例(改为 RouterLink):

-<button
+<RouterLink
   class="w-full rounded-xl px-3 py-2 text-left transition"
   :class="[...]"
-  type="button"
-  @click="conversationStore.selectConversation(item.id)"
+  :to="{ name: 'chat-with-id', params: { id: item.id } }"
+  @click.native="conversationStore.selectConversation(item.id)"
>
  ...
-</button>
+</RouterLink>

66-69: updated_at 可能为空/非法时的显示兜底。

建议为 formatRelativeTime(item.updated_at) 添加兜底(如 '-'),避免时间解析异常导致报错或显示 “Invalid Date”。

-<span>{{ formatRelativeTime(item.updated_at) }}</span>
+<span>{{ item.updated_at ? formatRelativeTime(item.updated_at) : '-' }}</span>

6-13: 为“新建会话”按钮补充可访问性文案。

仅显示“+”对屏幕阅读器不友好,建议添加 aria-label 或可隐藏的文本。

-<button ...>
-  <span class="text-lg leading-none">+</span>
-  {{ t('chat.newConversation') }}
-</button>
+<button aria-label="新建会话" ...>
+  <span class="text-lg leading-none" aria-hidden="true">+</span>
+  {{ t('chat.newConversation') }}
+  <span class="sr-only">{{ t('chat.newConversation') }}</span>
+</button>
ZaeXT/frontend/src/views/profile/ProfileView.vue (2)

53-58: 可使用 v-model.trim 避免因尾随空白导致“有更改”的假阳性。

减少用户无意的空白差异触发保存按钮。

-<textarea
-  v-model="memoryDraft"
+<textarea
+  v-model.trim="memoryDraft"

129-135: 返回逻辑健壮性良好,可再补一个兜底。

history.length > 1 但上一页不是站内页,router.back() 仍可能离站。可考虑在 back 后设置一个超时兜底或在入站时记录 from 决策。

ZaeXT/internal/handler/router.go (1)

14-21: 为 SSE 流式场景补充必要 Header(建议)。

前端以 POST+SSE 流式返回,建议在处理器中设置 Cache-Control: no-cacheConnection: keep-alive,并在 CORS 中允许 Last-Event-ID。CORS 已在上条 Diff 中加入。

ZaeXT/frontend/src/features/chat/ChatComposer.vue (1)

81-92: 默认模型选择的健壮性可再增强。

availableModels 异步变更时,若当前 selectedModel 已被移除,建议自动回退到第一个可用模型,避免发送时报“不可访问”。

 if (!models.length) return
-if (!selectedModel.value) {
+if (!selectedModel.value || !models.some(m => m.id === selectedModel.value)) {
   selectedModel.value = models[0]?.id
 }
ZaeXT/frontend/src/api/sse.ts (2)

71-105: SSE 事件未合并多行 data:,对部分实现不兼容。

SSE 允许同一事件多行 data: 合并,以 \n\n 作为事件分隔。当前逐行解析可能截断 JSON。建议在事件级别合并后再 JSON.parse

-    const processChunk = (chunk: string) => {
-      const lines = chunk.split('\n')
-      for (const line of lines) {
+    const processEvent = (eventText: string) => {
+      const lines = eventText.split('\n')
+      let dataLines: string[] = []
+      for (const line of lines) {
         if (!line.trim()) {
           continue
         }
         if (line.startsWith(':')) {
           // heartbeat line
           lastHeartbeat = Date.now()
           continue
         }
-        if (!line.startsWith('data:')) {
+        if (!line.startsWith('data:')) {
           continue
         }
-        const data = line.replace(/^data:\s*/, '')
+        const chunkData = line.replace(/^data:\s*/, '')
+        dataLines.push(chunkData)
-        if (data === DONE) {
+      }
+      const data = dataLines.join('\n')
+      if (data === DONE) {
         ...
-        }
-        try {
+      }
+      try {
         const parsed = JSON.parse(data) as OpenAIStreamChunk
         ...
-        } catch (error) {
+      } catch (error) {
         console.error('Failed to parse SSE chunk', data, error)
-        }
-      }
+      }
     }
@@
-    parts.forEach(processChunk)
+    parts.forEach(processEvent)

Also applies to: 115-118


128-140: 统一错误路径:避免多处 catch 分发相同错误。

结合前述 !ok 分支的修改,可将外层 catch 仅处理非 AbortError 且未回调过的情形,或在本地维护 errored 标志。

-  } catch (error) {
-    if ((error as Error).name !== 'AbortError') {
-      emitToast({ type: 'error', message: '消息发送失败,请稍后重试。' })
-      onError?.(error as Error)
-    }
-  }
+  } catch (error) {
+    if ((error as Error).name === 'AbortError') return controller
+    emitToast({ type: 'error', message: '消息发送失败,请稍后重试。' })
+    onError?.(error as Error)
+  }

Also applies to: 53-58

ZaeXT/frontend/src/i18n/index.ts (1)

6-6: 统一 Locale 类型,去掉多余断言,并为 localStorage 增加保护与同步 。

  • 目前对 i18n.global.locale 做了不必要的 as unknown as {...} 断言;在已传入泛型的情况下可直接赋值。
  • 读写 localStorage 未做 try/catch,Safari 隐私模式等场景可能抛错。
  • 多处硬编码 'zh-CN' | 'en-US' 建议提炼类型别名复用,并同步 document.documentElement.lang,利于无障碍/SEO。

可按下述 diff 简化与加固:

 import { createI18n } from 'vue-i18n'
 import enUS from './locales/en-US.json'
 import zhCN from './locales/zh-CN.json'

 export type AppMessageSchema = typeof zhCN
+export type SupportedLocale = 'zh-CN' | 'en-US'

 const STORAGE_KEY = 'ai-chat-lang'

-const locale =
-  (typeof window !== 'undefined' && window.localStorage.getItem(STORAGE_KEY)) || 'zh-CN'
+let saved: SupportedLocale | null = null
+try {
+  if (typeof window !== 'undefined') {
+    const raw = window.localStorage.getItem(STORAGE_KEY)
+    if (raw === 'zh-CN' || raw === 'en-US') saved = raw
+  }
+} catch {
+  // ignore storage errors
+}
+const locale: SupportedLocale = saved ?? 'zh-CN'

-export const i18n = createI18n<[AppMessageSchema], 'zh-CN' | 'en-US'>({
+export const i18n = createI18n<[AppMessageSchema], SupportedLocale>({
   legacy: false,
   globalInjection: true,
-  locale: locale === 'en-US' ? 'en-US' : 'zh-CN',
+  locale: locale === 'en-US' ? 'en-US' : 'zh-CN',
   fallbackLocale: 'zh-CN',
   messages: {
     'zh-CN': zhCN,
     'en-US': enUS,
   },
 })
 
-export const persistLocale = (value: 'zh-CN' | 'en-US') => {
+export const persistLocale = (value: SupportedLocale) => {
   if (typeof window === 'undefined') return
-  window.localStorage.setItem(STORAGE_KEY, value)
-  const localeRef = i18n.global.locale as unknown as { value: 'zh-CN' | 'en-US' }
-  localeRef.value = value
+  try {
+    window.localStorage.setItem(STORAGE_KEY, value)
+  } catch {
+    // ignore storage errors
+  }
+  i18n.global.locale.value = value
+  if (typeof document !== 'undefined') {
+    document.documentElement.lang = value
+  }
 }

Also applies to: 10-12, 13-22, 24-29

ZaeXT/frontend/src/stores/conversation.ts (2)

101-119: 高频流式更新下的性能问题:避免每个增量都 map 整个数组并克隆对象。

updateMessage 在流式回调中被频繁调用,但当前实现会对整列 map + 克隆,带来不必要的分配与 GC 压力,造成滚动卡顿。

可改为就地更新目标元素,避免重建数组:

-const updateMessage = (
-  conversationId: number,
-  messageId: number,
-  updater: (msg: ChatMessage) => void,
-) => {
-  const existing = messageMap.value[conversationId]
-  if (!existing) return
-  setMessages(
-    conversationId,
-    existing.map((msg) => {
-      if (msg.id === messageId) {
-        const clone = { ...msg }
-        updater(clone)
-        return clone
-      }
-      return msg
-    }),
-  )
-}
+const updateMessage = (
+  conversationId: number,
+  messageId: number,
+  updater: (msg: ChatMessage) => void,
+) => {
+  const arr = messageMap.value[conversationId]
+  if (!arr) return
+  const idx = arr.findIndex((m) => m.id === messageId)
+  if (idx === -1) return
+  updater(arr[idx]) // 就地修改即可触发 Vue3 响应式
+}

如需进一步平滑,可在 onDelta 中使用 requestAnimationFrame/节流聚合更新。


37-54: 临时消息 ID 生成不一致且可能与后端正整数 ID 冲突。

当前用户消息用正数 Date.now(),助手占位用负数时间戳。建议统一使用自减的负数临时 ID,彻底规避与后端自增 ID 的碰撞。

 interface StreamingState { ... }
 
+// 统一临时 ID 生成器(负数递减)
+let __tmpId = -1
+const nextTmpId = () => __tmpId--
+
 const createAssistantPlaceholder = (conversationId: number): ChatMessage => ({
-  id: Date.now() * -1,
+  id: nextTmpId(),
   conversation_id: conversationId,
   role: 'assistant',
   content: '',
   created_at: new Date().toISOString(),
   status: 'streaming',
 })
 
 const createUserMessage = (conversationId: number, content: string): ChatMessage => ({
-  id: Date.now(),
+  id: nextTmpId(),
   conversation_id: conversationId,
   role: 'user',
   content,
   created_at: new Date().toISOString(),
   status: 'done',
 })

请确认后端返回的消息 ID 是否始终为正整数;若有其他范围约定,请同步此生成器的策略。

Also applies to: 46-53

ZaeXT/frontend/src/utils/notifications.ts (1)

19-25: 为 Toast 自动补全唯一 id,便于列表 key 与防重复。

如果调用方未提供 id,可以默认生成一个,减少 UI 侧去重/过渡问题:

 export const emitToast = (payload: ToastPayload) => {
   emitter.emit('toast', {
+    id: payload.id ?? (globalThis.crypto?.randomUUID?.() ?? String(Date.now())),
     type: 'info',
     duration: 3000,
     ...payload,
   })
 }
ZaeXT/internal/handler/chat_handler.go (1)

200-205: SSE 错误序列化应处理 Marshal 失败

虽然已改用 json.Marshal 构建错误消息(避免了手工拼接 JSON 的转义问题),但 Marshal 的错误仍被忽略。建议捕获该错误,若序列化失败则回退到安全的字面量消息。

应用此修复:

-errorPayload := gin.H{"error": streamError.Error()}
-jsonData, _ := json.Marshal(errorPayload)
-fmt.Fprintf(c.Writer, "data: %s\n\n", jsonData)
+errorPayload := gin.H{"error": streamError.Error()}
+jsonData, err := json.Marshal(errorPayload)
+if err != nil {
+  jsonData = []byte(`{"error":"internal error"}`)
+}
+fmt.Fprintf(c.Writer, "data: %s\n\n", jsonData)
ZaeXT/frontend/package.json (2)

18-73: 考虑统一依赖版本锁定策略

所有运行时和开发依赖均使用caret (^) 版本符号,这允许自动小版本更新。虽然TypeScript明智地使用了tilde (~) 进行更保守的版本控制,但建议整个项目统一采用更严格的版本锁定策略。

建议在package-lock.json或yarn.lock中锁定确切版本,或者考虑对关键依赖使用符号。特别是对于Vue、Vue Router、Pinia等核心框架依赖,建议采用tilde () 而非caret (^)。


12-12: 建议添加自动格式化脚本

第12行的format脚本使用了--check标志,仅进行格式检查而不自动修复。建议添加一个自动格式化脚本以提高开发效率。

推荐添加:"format:fix": "prettier --write .",这样开发者可以自动修复格式问题。

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e8c3e45 and c06ea35.

⛔ Files ignored due to path filters (3)
  • ZaeXT/frontend/package-lock.json is excluded by !**/package-lock.json
  • ZaeXT/frontend/public/vite.svg is excluded by !**/*.svg
  • ZaeXT/frontend/src/assets/vue.svg is excluded by !**/*.svg
📒 Files selected for processing (89)
  • .gitignore (1 hunks)
  • ZaeXT/frontend/.env (1 hunks)
  • ZaeXT/frontend/.env.example (1 hunks)
  • ZaeXT/frontend/.eslintignore (1 hunks)
  • ZaeXT/frontend/.eslintrc.cjs (1 hunks)
  • ZaeXT/frontend/.gitignore (1 hunks)
  • ZaeXT/frontend/.prettierignore (1 hunks)
  • ZaeXT/frontend/.prettierrc.cjs (1 hunks)
  • ZaeXT/frontend/.stylelintignore (1 hunks)
  • ZaeXT/frontend/.stylelintrc.cjs (1 hunks)
  • ZaeXT/frontend/.vscode/extensions.json (1 hunks)
  • ZaeXT/frontend/README.md (1 hunks)
  • ZaeXT/frontend/index.html (1 hunks)
  • ZaeXT/frontend/package.json (1 hunks)
  • ZaeXT/frontend/postcss.config.js (1 hunks)
  • ZaeXT/frontend/src/App.vue (1 hunks)
  • ZaeXT/frontend/src/api/__tests__/http.spec.ts (1 hunks)
  • ZaeXT/frontend/src/api/__tests__/sse.spec.ts (1 hunks)
  • ZaeXT/frontend/src/api/auth.ts (1 hunks)
  • ZaeXT/frontend/src/api/categories.ts (1 hunks)
  • ZaeXT/frontend/src/api/conversations.ts (1 hunks)
  • ZaeXT/frontend/src/api/http.ts (1 hunks)
  • ZaeXT/frontend/src/api/index.ts (1 hunks)
  • ZaeXT/frontend/src/api/models.ts (1 hunks)
  • ZaeXT/frontend/src/api/recycle-bin.ts (1 hunks)
  • ZaeXT/frontend/src/api/sse.ts (1 hunks)
  • ZaeXT/frontend/src/api/user.ts (1 hunks)
  • ZaeXT/frontend/src/components/AppEmptyState.vue (1 hunks)
  • ZaeXT/frontend/src/components/AppErrorState.vue (1 hunks)
  • ZaeXT/frontend/src/components/AppLoading.vue (1 hunks)
  • ZaeXT/frontend/src/components/AppMarkdownRenderer.vue (1 hunks)
  • ZaeXT/frontend/src/components/AppSettingsWidget.vue (1 hunks)
  • ZaeXT/frontend/src/components/AppToaster.vue (1 hunks)
  • ZaeXT/frontend/src/env.d.ts (1 hunks)
  • ZaeXT/frontend/src/features/categories/CategoryNode.vue (1 hunks)
  • ZaeXT/frontend/src/features/chat/ChatComposer.vue (1 hunks)
  • ZaeXT/frontend/src/features/chat/ChatHeader.vue (1 hunks)
  • ZaeXT/frontend/src/features/chat/ChatInfoPanel.vue (1 hunks)
  • ZaeXT/frontend/src/features/chat/ChatMessageList.vue (1 hunks)
  • ZaeXT/frontend/src/features/chat/ChatSidebar.vue (1 hunks)
  • ZaeXT/frontend/src/i18n/index.ts (1 hunks)
  • ZaeXT/frontend/src/i18n/locales/en-US.json (1 hunks)
  • ZaeXT/frontend/src/i18n/locales/zh-CN.json (1 hunks)
  • ZaeXT/frontend/src/main.ts (1 hunks)
  • ZaeXT/frontend/src/router/index.ts (1 hunks)
  • ZaeXT/frontend/src/stores/auth.ts (1 hunks)
  • ZaeXT/frontend/src/stores/category.ts (1 hunks)
  • ZaeXT/frontend/src/stores/conversation.ts (1 hunks)
  • ZaeXT/frontend/src/stores/index.ts (1 hunks)
  • ZaeXT/frontend/src/stores/model.ts (1 hunks)
  • ZaeXT/frontend/src/stores/recycle-bin.ts (1 hunks)
  • ZaeXT/frontend/src/styles/main.css (1 hunks)
  • ZaeXT/frontend/src/types/api.ts (1 hunks)
  • ZaeXT/frontend/src/types/auth.ts (1 hunks)
  • ZaeXT/frontend/src/types/category.ts (1 hunks)
  • ZaeXT/frontend/src/types/conversation.ts (1 hunks)
  • ZaeXT/frontend/src/types/index.ts (1 hunks)
  • ZaeXT/frontend/src/types/model.ts (1 hunks)
  • ZaeXT/frontend/src/types/recycle-bin.ts (1 hunks)
  • ZaeXT/frontend/src/types/shims-virtual-scroller.d.ts (1 hunks)
  • ZaeXT/frontend/src/types/sse.ts (1 hunks)
  • ZaeXT/frontend/src/types/user.ts (1 hunks)
  • ZaeXT/frontend/src/utils/event-bus.ts (1 hunks)
  • ZaeXT/frontend/src/utils/jwt.ts (1 hunks)
  • ZaeXT/frontend/src/utils/notifications.ts (1 hunks)
  • ZaeXT/frontend/src/utils/time.ts (1 hunks)
  • ZaeXT/frontend/src/utils/token.ts (1 hunks)
  • ZaeXT/frontend/src/views/auth/LoginView.vue (1 hunks)
  • ZaeXT/frontend/src/views/auth/RegisterView.vue (1 hunks)
  • ZaeXT/frontend/src/views/categories/CategoriesView.vue (1 hunks)
  • ZaeXT/frontend/src/views/chat/ChatWorkspace.vue (1 hunks)
  • ZaeXT/frontend/src/views/errors/ErrorView.vue (1 hunks)
  • ZaeXT/frontend/src/views/models/ModelsView.vue (1 hunks)
  • ZaeXT/frontend/src/views/profile/ProfileView.vue (1 hunks)
  • ZaeXT/frontend/src/views/recycle-bin/RecycleBinView.vue (1 hunks)
  • ZaeXT/frontend/tailwind.config.ts (1 hunks)
  • ZaeXT/frontend/tsconfig.app.json (1 hunks)
  • ZaeXT/frontend/tsconfig.json (1 hunks)
  • ZaeXT/frontend/tsconfig.node.json (1 hunks)
  • ZaeXT/frontend/vite.config.ts (1 hunks)
  • ZaeXT/frontend/vitest.setup.ts (1 hunks)
  • ZaeXT/internal/adapter/volcengine/doubao_client.go (1 hunks)
  • ZaeXT/internal/handler/chat_handler.go (1 hunks)
  • ZaeXT/internal/handler/request/chat.go (1 hunks)
  • ZaeXT/internal/handler/request/conversation.go (1 hunks)
  • ZaeXT/internal/handler/response/messages.go (1 hunks)
  • ZaeXT/internal/handler/response/sse.go (1 hunks)
  • ZaeXT/internal/handler/router.go (1 hunks)
  • ZaeXT/internal/service/chat_service.go (1 hunks)
✅ Files skipped from review due to trivial changes (6)
  • ZaeXT/frontend/.eslintignore
  • ZaeXT/frontend/tsconfig.app.json
  • ZaeXT/frontend/.env
  • ZaeXT/frontend/.env.example
  • ZaeXT/frontend/.vscode/extensions.json
  • ZaeXT/frontend/src/styles/main.css
🚧 Files skipped from review as they are similar to previous changes (2)
  • .gitignore
  • ZaeXT/internal/handler/request/chat.go
🧰 Additional context used
🧬 Code graph analysis (26)
ZaeXT/frontend/src/api/models.ts (2)
ZaeXT/frontend/src/api/http.ts (1)
  • http (113-113)
ZaeXT/frontend/src/types/model.ts (1)
  • ModelInfo (3-9)
ZaeXT/frontend/src/stores/conversation.ts (5)
ZaeXT/frontend/src/types/conversation.ts (6)
  • ConversationMessage (16-23)
  • ConversationSummary (3-14)
  • CreateConversationPayload (29-32)
  • UpdateConversationTitlePayload (34-36)
  • UpdateConversationCategoryPayload (38-40)
  • SendMessagePayload (47-51)
ZaeXT/frontend/src/types/sse.ts (1)
  • StreamController (34-36)
ZaeXT/frontend/src/api/conversations.ts (7)
  • fetchConversations (15-16)
  • fetchConversationMessages (18-21)
  • createConversation (12-13)
  • updateConversationTitle (23-30)
  • updateConversationCategory (32-39)
  • triggerAutoClassify (41-44)
  • deleteConversation (46-47)
ZaeXT/frontend/src/utils/notifications.ts (1)
  • emitToast (19-25)
ZaeXT/frontend/src/api/sse.ts (1)
  • streamConversation (31-143)
ZaeXT/frontend/src/api/user.ts (2)
ZaeXT/frontend/src/api/http.ts (1)
  • http (113-113)
ZaeXT/frontend/src/types/user.ts (2)
  • UserProfile (3-9)
  • UpdateMemoryPayload (11-13)
ZaeXT/frontend/src/types/recycle-bin.ts (1)
ZaeXT/frontend/src/types/conversation.ts (1)
  • ConversationSummary (3-14)
ZaeXT/frontend/src/api/sse.ts (4)
ZaeXT/frontend/src/types/sse.ts (3)
  • StreamCallbacks (28-32)
  • StreamController (34-36)
  • OpenAIStreamChunk (18-24)
ZaeXT/frontend/src/types/conversation.ts (1)
  • SendMessagePayload (47-51)
ZaeXT/frontend/src/utils/token.ts (1)
  • getAuthHeader (16-19)
ZaeXT/frontend/src/utils/notifications.ts (1)
  • emitToast (19-25)
ZaeXT/frontend/src/stores/category.ts (3)
ZaeXT/frontend/src/types/category.ts (3)
  • CategoryTreeNode (1-8)
  • CreateCategoryPayload (10-13)
  • UpdateCategoryPayload (15-18)
ZaeXT/frontend/src/api/categories.ts (4)
  • fetchCategories (5-5)
  • createCategory (7-8)
  • updateCategory (10-11)
  • deleteCategory (13-13)
ZaeXT/frontend/src/utils/notifications.ts (1)
  • emitToast (19-25)
ZaeXT/frontend/src/utils/jwt.ts (1)
KimmyXYC/web/js/api.js (1)
  • token (5-5)
ZaeXT/frontend/src/types/model.ts (1)
ZaeXT/frontend/src/types/user.ts (1)
  • UserTier (1-1)
ZaeXT/frontend/src/stores/auth.ts (7)
ZaeXT/frontend/src/utils/token.ts (3)
  • getToken (3-4)
  • setToken (6-9)
  • clearToken (11-14)
ZaeXT/frontend/src/types/user.ts (2)
  • UserProfile (3-9)
  • UpdateMemoryPayload (11-13)
ZaeXT/frontend/src/utils/event-bus.ts (1)
  • appEventBus (7-7)
ZaeXT/frontend/src/api/user.ts (2)
  • getProfile (5-5)
  • updateMemory (7-8)
ZaeXT/frontend/src/types/auth.ts (2)
  • LoginPayload (6-6)
  • RegisterPayload (1-4)
ZaeXT/frontend/src/api/auth.ts (2)
  • login (7-8)
  • register (5-5)
ZaeXT/frontend/src/utils/notifications.ts (1)
  • emitToast (19-25)
ZaeXT/frontend/src/utils/token.ts (1)
KimmyXYC/web/js/api.js (1)
  • token (5-5)
ZaeXT/frontend/src/api/http.ts (4)
ZaeXT/frontend/src/utils/token.ts (2)
  • getToken (3-4)
  • clearToken (11-14)
ZaeXT/frontend/src/types/api.ts (2)
  • ApiResp (1-5)
  • ApiErrorPayload (16-20)
ZaeXT/frontend/src/utils/notifications.ts (1)
  • emitToast (19-25)
ZaeXT/frontend/src/utils/event-bus.ts (1)
  • appEventBus (7-7)
ZaeXT/frontend/src/stores/model.ts (3)
ZaeXT/frontend/src/types/model.ts (1)
  • ModelInfo (3-9)
ZaeXT/frontend/src/stores/auth.ts (1)
  • useAuthStore (10-101)
ZaeXT/frontend/src/api/models.ts (1)
  • fetchModels (5-5)
ZaeXT/frontend/src/router/index.ts (2)
ZaeXT/frontend/src/utils/token.ts (1)
  • TOKEN_KEY (21-21)
ZaeXT/frontend/src/stores/auth.ts (1)
  • useAuthStore (10-101)
ZaeXT/frontend/src/api/__tests__/http.spec.ts (1)
ZaeXT/frontend/src/api/http.ts (2)
  • http (113-113)
  • ApiError (11-21)
ZaeXT/frontend/src/api/conversations.ts (2)
ZaeXT/frontend/src/types/conversation.ts (6)
  • CreateConversationPayload (29-32)
  • ConversationSummary (3-14)
  • ConversationMessage (16-23)
  • UpdateConversationTitlePayload (34-36)
  • UpdateConversationCategoryPayload (38-40)
  • AutoClassifyResponse (42-45)
ZaeXT/frontend/src/api/http.ts (1)
  • http (113-113)
ZaeXT/frontend/src/api/auth.ts (2)
ZaeXT/frontend/src/types/auth.ts (3)
  • RegisterPayload (1-4)
  • LoginPayload (6-6)
  • LoginResponse (8-10)
ZaeXT/frontend/src/api/http.ts (1)
  • http (113-113)
ZaeXT/frontend/src/main.ts (2)
ZaeXT/frontend/src/stores/index.ts (1)
  • pinia (3-3)
ZaeXT/frontend/src/i18n/index.ts (1)
  • i18n (13-22)
ZaeXT/internal/adapter/volcengine/doubao_client.go (2)
ZaeXT/internal/model/message.go (1)
  • Message (3-10)
ZaeXT/internal/configs/config.go (2)
  • ModelInfo (41-45)
  • Conf (10-10)
ZaeXT/frontend/src/stores/recycle-bin.ts (3)
ZaeXT/frontend/src/types/recycle-bin.ts (1)
  • RecycleBinItem (3-5)
ZaeXT/frontend/src/api/recycle-bin.ts (3)
  • fetchRecycleBinItems (5-6)
  • restoreConversation (8-9)
  • deleteConversationPermanently (11-12)
ZaeXT/frontend/src/utils/notifications.ts (1)
  • emitToast (19-25)
ZaeXT/internal/service/chat_service.go (9)
ZaeXT/internal/adapter/volcengine/doubao_client.go (2)
  • ChatRequest (45-48)
  • AvailableModel (50-53)
ZaeXT/internal/handler/request/chat.go (1)
  • CreateConversation (9-12)
ZaeXT/internal/model/conversation.go (1)
  • Conversation (5-16)
ZaeXT/internal/model/message.go (1)
  • Message (3-10)
ZaeXT/internal/handler/request/conversation.go (1)
  • UpdateConversationCategory (3-5)
ZaeXT/internal/repository/conversation_repo.go (1)
  • ConversationRepository (10-20)
ZaeXT/internal/repository/message_repo.go (1)
  • MessageRepository (9-13)
ZaeXT/internal/repository/user_repo.go (1)
  • UserRepository (9-14)
ZaeXT/internal/repository/category_repo.go (1)
  • CategoryRepository (9-15)
ZaeXT/frontend/src/types/sse.ts (1)
ZaeXT/frontend/src/types/conversation.ts (1)
  • MessageRole (1-1)
ZaeXT/frontend/src/api/recycle-bin.ts (2)
ZaeXT/frontend/src/api/http.ts (1)
  • http (113-113)
ZaeXT/frontend/src/types/recycle-bin.ts (1)
  • RecycleBinItem (3-5)
ZaeXT/frontend/src/api/__tests__/sse.spec.ts (1)
ZaeXT/frontend/src/api/sse.ts (1)
  • streamConversation (31-143)
ZaeXT/internal/handler/router.go (8)
ZaeXT/internal/service/service.go (1)
  • Service (7-12)
ZaeXT/internal/handler/user_handler.go (1)
  • NewUserHandler (18-20)
ZaeXT/internal/handler/chat_handler.go (1)
  • NewChatHandler (22-24)
ZaeXT/internal/handler/category_handler.go (1)
  • NewCategoryHandler (18-20)
ZaeXT/internal/handler/recycle_bin_handler.go (1)
  • NewRecycleBinHandler (18-20)
ZaeXT/internal/handler/middleware/auth.go (1)
  • AuthMiddleware (13-42)
ZaeXT/internal/handler/request/chat.go (2)
  • CreateConversation (9-12)
  • UpdateTitle (14-16)
ZaeXT/internal/handler/request/conversation.go (1)
  • UpdateConversationCategory (3-5)
ZaeXT/internal/handler/chat_handler.go (9)
ZaeXT/internal/service/chat_service.go (1)
  • ChatService (19-30)
ZaeXT/internal/handler/response/response.go (2)
  • Fail (28-38)
  • Success (20-26)
ZaeXT/internal/pkg/e/code.go (5)
  • InvalidParams (6-6)
  • Error (5-5)
  • Success (4-4)
  • PermissionDenied (8-8)
  • NotFound (9-9)
ZaeXT/internal/handler/request/conversation.go (1)
  • UpdateConversationCategory (3-5)
ZaeXT/internal/handler/request/chat.go (3)
  • CreateConversation (9-12)
  • UpdateTitle (14-16)
  • ChatMessage (3-7)
ZaeXT/internal/handler/response/chat.go (1)
  • ConversationInfo (5-13)
ZaeXT/internal/model/message.go (1)
  • Message (3-10)
ZaeXT/internal/model/conversation.go (1)
  • Conversation (5-16)
ZaeXT/internal/handler/response/messages.go (1)
  • MessageInfo (5-10)
ZaeXT/frontend/src/api/categories.ts (2)
ZaeXT/frontend/src/api/http.ts (1)
  • http (113-113)
ZaeXT/frontend/src/types/category.ts (3)
  • CategoryTreeNode (1-8)
  • CreateCategoryPayload (10-13)
  • UpdateCategoryPayload (15-18)
🪛 LanguageTool
ZaeXT/frontend/README.md

[uncategorized] ~8-~8: 您的意思是“由"于"”吗?
Context: ...合 Pinia 管理全局状态 - Vue Router 4:路由与守卫,登陆态校验 & 进度条提示 - TypeScript 严格模式:D...

(YU7_YU8)

🪛 markdownlint-cli2 (0.18.1)
ZaeXT/frontend/README.md

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

(MD040, fenced-code-language)

🔇 Additional comments (77)
ZaeXT/frontend/README.md (2)

94-105: 验证后端端点实现与文档一致性。

README 详细描述了后端集成点,但应确保 Go + Gin 后端确实实现了所有列举的 REST 端点和 SSE 流响应格式。建议在后端 PR 或集成测试中确认。


1-31: 文档结构清晰、内容全面,架构设计说明深思熟虑。

这份 README 为前端项目提供了优秀的入门与参考指南:

  • 技术栈:准确列举了核心依赖与工程化工具链
  • 功能覆盖:从认证、SSE 聊天、会话管理、权限控制到 i18n 和主题切换,阐述完整
  • 目录结构:模块划分清晰(api / components / stores / router / types 等)
  • 设计原则:KISS、SOLID、DRY 等原则的阐述展示了成熟的工程理念
  • 维护指南:关于 DTO 优先策略、i18n 扩展与已知问题的建议具有实用价值
ZaeXT/frontend/src/types/shims-virtual-scroller.d.ts (1)

1-1: 确认 @types 包不存在,改进方案

验证结果显示 @types/vue3-virtual-scroller 在 npm 上不存在。vue3-virtual-scroller 是一个 fork 项目,原意为应急使用,建议使用官方支持的 vue-virtual-scroller@next。

vue3-virtual-scroller 库不支持从 npm 加载类型,当前的最小化声明是合理的临时方案。建议:

  1. 如果继续使用 vue3-virtual-scroller,考虑添加更详细的类型定义,至少包含实际使用的组件和事件
  2. 或迁移至官方 vue-virtual-scroller 包(它有更好的 Vue 3 支持和社区类型定义可用)
ZaeXT/frontend/.prettierrc.cjs (1)

1-8: 配置合理,代码格式化设置符合最佳实践。

Prettier 配置采用了常见的格式化规范:无分号、单引号、100 字符行宽、尾随逗号、2 空格缩进和 LF 换行符。这些设置在现代前端项目中很常见且易于维护。

ZaeXT/frontend/.prettierignore (1)

1-7: 忽略模式配置正确。

包含了标准的忽略目录和文件:依赖包、构建产物、测试覆盖率报告、日志文件以及编辑器配置目录。这些都是应该排除在格式化之外的常见文件。

ZaeXT/frontend/.gitignore (1)

1-24: Git 忽略规则完善且遵循最佳实践。

配置覆盖全面:

  • 各类日志文件(npm/yarn/pnpm/lerna)
  • 依赖和构建产物
  • 编辑器配置目录(同时保留 .vscode/extensions.json 以便团队共享推荐扩展)
  • 操作系统特定文件(.DS_Store 等)
ZaeXT/frontend/.eslintrc.cjs (1)

1-40: ESLint 配置完善,适配 Vue 3 + TypeScript 项目。

配置亮点:

  • 正确设置 Vue 3 解析器和 TypeScript 支持
  • 集成 Prettier 避免格式冲突
  • 使用 simple-import-sort 强制导入顺序一致
  • 未使用变量规则允许下划线前缀(^_),这是处理回调参数的良好实践

注意:vue/multi-word-component-namesvue/no-v-html 规则已关闭。如果项目中有安全考虑(如处理用户输入的 HTML),建议重新评估 vue/no-v-html 规则。

ZaeXT/frontend/.stylelintignore (1)

1-4: Stylelint 忽略配置合理。

忽略了构建产物、依赖包、测试覆盖率报告以及 public 静态资源目录,这些目录通常不需要样式检查。

ZaeXT/frontend/src/env.d.ts (1)

9-11: LGTM!

ImportMeta 接口的类型扩展正确,符合 Vite 的类型约定。

ZaeXT/frontend/tsconfig.json (1)

1-7: 根 tsconfig 结构合理,可直接用于 tsc -b

空 files + references 的组合式配置符合预期,无需改动。

ZaeXT/frontend/src/utils/jwt.ts (1)

26-32: LGTM!

token 过期检查的逻辑正确,正确处理了 null token 和缺少 exp 声明的情况。

ZaeXT/frontend/src/types/sse.ts (1)

1-36: LGTM!

SSE 类型定义清晰完整,与后端的流式响应结构对齐良好。接口设计合理,支持流式传输的各种场景。

ZaeXT/frontend/src/types/user.ts (1)

1-13: LGTM!

用户类型定义结构清晰。UserTier 类型的设计允许扩展性(literal union + string),这在需要支持未来新增层级时很有用。

ZaeXT/frontend/src/components/AppMarkdownRenderer.vue (2)

17-32: LGTM!

Markdown 配置合理,禁用 HTML 并使用语法高亮是良好的安全实践。对于无法识别的语言,正确地回退到转义 HTML。


80-83: LGTM!

在组件卸载时清理复制按钮是良好的实践,避免内存泄漏。

ZaeXT/frontend/src/features/chat/ChatInfoPanel.vue (2)

83-93: LGTM!

分类选项的递归遍历逻辑实现正确,通过深度参数正确地缩进子分类,UI 层级清晰。


103-119: LGTM!

分类更新和自动分类功能的实现逻辑正确:

  • 正确处理字符串到数字的转换
  • 自动分类后正确更新选中状态
  • 操作后及时刷新分类数据
ZaeXT/frontend/vitest.setup.ts (1)

1-7: LGTM!

测试设置文件配置正确,引入 fetch polyfill 并在每个测试后恢复 mock,这是标准的 Vitest 最佳实践。

ZaeXT/frontend/src/App.vue (1)

1-12: LGTM!

根组件结构清晰,正确组合了路由视图和全局 UI 组件(通知和设置部件)。

ZaeXT/frontend/src/main.ts (1)

12-23: LGTM!

应用启动流程配置正确:

  • 插件安装顺序合理(Pinia 在 router 之前)
  • 在挂载前等待 router.isReady() 是良好实践,避免导航相关的竞态条件
  • 使用 async/await 清晰处理异步初始化
ZaeXT/frontend/src/types/model.ts (1)

3-9: 类型定义清晰合理!

ModelInfo 接口设计良好,字段定义准确,可选字段使用恰当。该类型与后端的 ModelInfo 结构保持一致,便于前后端数据交互。

ZaeXT/frontend/src/utils/event-bus.ts (1)

1-7: 事件总线实现简洁有效!

使用 mitt 库创建类型安全的事件总线是一个好的实践。当前仅定义了 auth:unauthorized 事件,随着应用发展可能需要添加更多事件类型。

ZaeXT/frontend/src/api/models.ts (1)

5-5: API 调用实现正确!

fetchModels 函数正确地使用了 HTTP 客户端获取模型列表。泛型参数 <ModelInfo[], ModelInfo[]> 的双重声明可能是为了适配 HTTP 客户端的类型签名,如果实际响应和返回数据类型相同,可以考虑简化。

ZaeXT/frontend/src/types/auth.ts (1)

1-10: 认证类型定义简洁明确!

类型定义准确且易于理解。使用类型别名 LoginPayload = RegisterPayload 在语义上区分了登录和注册的载荷,同时避免了代码重复。

ZaeXT/frontend/src/types/recycle-bin.ts (1)

1-7: 回收站类型定义清晰合理!

通过扩展 ConversationSummary 来定义 RecycleBinItem 是一个好的设计模式,避免了字段重复。deleted_at 字段的添加准确反映了回收站项目的特性。

ZaeXT/frontend/index.html (1)

1-18: 代码结构良好,favicon 配置正确 ✓

HTML 入口文件结构清晰,包含了必要的 meta 标签和正确的 Vite 模块加载配置。主题颜色与 Tailwind 配置中的品牌色保持一致。

验证结果:/vite.svg 文件已确认存在于 ZaeXT/frontend/public/vite.svg,Vite 会正确地将其提供服务,favicon 加载无问题。

ZaeXT/frontend/src/views/errors/ErrorView.vue (1)

22-38: 代码实现正确。

脚本部分使用了正确的 Vue 3 Composition API 模式,类型定义清晰,props 配置合理。

ZaeXT/frontend/postcss.config.js (1)

1-6: 标准的 PostCSS 配置,正确集成了 Tailwind CSS。

配置文件正确引入了 tailwindcss 和 autoprefixer 插件,这是 Tailwind CSS 项目的标准配置。

ZaeXT/internal/handler/response/messages.go (1)

1-10: 响应结构体定义正确。

MessageInfo 结构体设计简洁明了,字段类型合适,JSON 标签遵循了 snake_case 命名约定,符合 Go 语言的最佳实践。

ZaeXT/frontend/src/api/index.ts (1)

1-7: 标准的桶式导出模式,实现正确。

该文件作为 API 模块的统一入口点,使用 export * 语法重新导出各个子模块,简化了消费代码的导入路径,是 TypeScript 项目中的常见做法。

ZaeXT/frontend/src/api/user.ts (1)

1-8: API 函数实现正确且简洁。

两个函数都正确使用了共享的 http 客户端,类型参数指定准确,路径和方法符合 RESTful 规范。代码遵循了项目中已建立的 API 模块模式。

ZaeXT/frontend/src/i18n/locales/en-US.json (1)

1-117: 翻译文案整体质量良好。

英文翻译覆盖全面,语法正确,术语使用一致。占位符语法(如 {time}{name})使用正确。文案组织清晰,按功能模块分组。

ZaeXT/frontend/src/api/recycle-bin.ts (1)

1-12: 回收站 API 函数实现正确。

三个函数都正确使用了共享的 http 客户端,HTTP 方法选择恰当(GET、POST、DELETE),类型参数准确,路径参数使用正确。代码简洁且遵循了项目中建立的 API 模块模式。

ZaeXT/frontend/src/components/AppEmptyState.vue (1)

1-16: 实现简洁且符合最佳实践。

这个空状态组件设计良好,使用插槽提供了良好的自定义能力,默认值合理,样式支持深色模式。组件职责单一,易于维护和复用。

ZaeXT/frontend/src/api/auth.ts (1)

5-8: API 封装清晰且类型安全。

认证接口的实现简洁明了,正确使用了泛型类型参数,与项目中其他 API 模块保持一致的风格。

ZaeXT/frontend/src/types/category.ts (1)

1-18: 类型定义完整且结构合理。

分类的类型定义支持递归树形结构,正确处理了根节点和子节点的场景(通过可空的 parent_id),时间戳字段完整,创建和更新的 payload 结构清晰。

ZaeXT/frontend/src/views/models/ModelsView.vue (1)

1-67: 组件实现规范且用户体验良好。

模型视图正确使用了 Pinia store 和 Composition API,在挂载时加载数据,通过视觉样式清晰区分可用和锁定的模型。国际化处理完整,响应式布局合理。

ZaeXT/frontend/src/stores/index.ts (1)

1-9: store 组织结构清晰。

使用标准的桶式导出模式,创建单一的 Pinia 实例并集中导出所有 store 模块,便于统一管理和使用。

ZaeXT/frontend/src/views/recycle-bin/RecycleBinView.vue (1)

1-65: UI 组件使用恰当,交互设计合理。

回收站视图正确处理了加载、空状态和列表展示的各种场景,恢复和永久删除操作的按钮在加载时正确禁用,永久删除前有确认提示,用户体验良好。

ZaeXT/frontend/src/stores/category.ts (1)

28-58: store actions 实现合理,状态管理清晰。

加载和变更操作都有适当的错误处理和用户反馈。变更操作后重新加载分类树确保了 UI 的一致性。并发加载的防护机制(line 29)避免了重复请求。

ZaeXT/frontend/src/api/__tests__/sse.spec.ts (1)

1-87: 测试覆盖全面且实现规范。

SSE 流式传输的测试设计良好,覆盖了成功流和错误处理两个主要场景。Mock 设置清晰,异步协调使用 Promise 处理得当,断言验证了关键行为(delta 累积、回调调用次数、错误传播)。

ZaeXT/frontend/src/api/__tests__/http.spec.ts (1)

33-91: 测试覆盖全面,实现正确。

该测试套件全面覆盖了HTTP客户端的核心行为:

  • 认证头部附加
  • 业务错误处理
  • 各种HTTP状态码的错误处理和路由跳转
  • Token清除和登录重定向

测试结构清晰,使用了适当的mock策略。

ZaeXT/frontend/src/components/AppToaster.vue (1)

56-84: Toast管理逻辑实现正确。

该组件正确实现了:

  • ID生成(使用crypto.randomUUID并提供降级方案)
  • 自动关闭定时器的设置和清理
  • 手动关闭时正确清理定时器,避免内存泄漏

实现符合最佳实践。

ZaeXT/frontend/src/views/categories/CategoriesView.vue (1)

74-80: 返回逻辑实现正确。

正确检查了浏览器历史记录长度,避免在无历史记录时调用 router.back() 导致的问题。降级到聊天页面的逻辑也很合理。

ZaeXT/frontend/src/views/auth/RegisterView.vue (1)

86-93: 注册逻辑实现正确。

正确验证了两次密码输入的一致性,并在密码不匹配时给出用户提示。注册成功后跳转到登录页的流程也符合常见的用户体验模式。

ZaeXT/frontend/src/utils/time.ts (1)

1-17: 时间工具实现正确。

dayjs配置正确,包括:

  • 中文语言包加载
  • relativeTime和utc插件扩展
  • 提供的三个工具函数接口清晰、实现简洁

实现符合预期。

ZaeXT/frontend/src/views/chat/ChatWorkspace.vue (2)

95-103: 并行数据加载实现优秀。

使用 Promise.all 并行加载会话、模型和分类数据,这种方式能显著提升页面加载性能,减少用户等待时间。这是一个很好的性能优化实践。


116-124: 路由同步逻辑正确。

正确实现了会话ID与路由参数的双向同步,并在第120行添加了去重检查,避免不必要的路由更新。这种防御性编程能有效避免潜在的无限循环问题。

ZaeXT/frontend/src/stores/recycle-bin.ts (1)

14-24: 加载逻辑实现正确。

使用了加载状态保护(第15行),避免重复请求。错误处理和finally块的使用也很恰当,确保loading状态能正确重置。

ZaeXT/frontend/src/views/auth/LoginView.vue (2)

71-75: 已登录用户自动跳转实现正确。

在组件挂载时检查用户是否已登录,如果已登录则自动跳转到聊天页面。这是良好的用户体验设计,避免已登录用户看到不必要的登录表单。


77-81: 登录重定向逻辑实现正确。

正确处理了 redirect 查询参数,登录成功后跳转到之前尝试访问的页面,如果没有指定则跳转到根路径。这与路由守卫的重定向逻辑配合良好。

ZaeXT/frontend/src/api/categories.ts (1)

1-13: API 模块实现正确!

分类 CRUD 操作的实现简洁清晰,类型安全且符合 REST 规范。

ZaeXT/frontend/src/stores/auth.ts (1)

49-59: 登录实现正确

登录流程清晰,状态管理得当。注意如果 fetchProfileSafely 失败,用户将有 token 但无 profile 数据,不过该函数内部已有错误处理,这种设计是可接受的。

ZaeXT/frontend/src/types/index.ts (1)

1-8: 类型聚合导出实现正确

标准的 barrel export 模式,简化了类型导入。实现简洁明了。

ZaeXT/frontend/vite.config.ts (1)

39-42: 开发服务器暴露于网络

host: '0.0.0.0' 将开发服务器暴露给网络中的所有设备,可能存在安全风险。除非需要从其他设备访问(如移动端测试),否则建议使用默认的 localhost

ZaeXT/frontend/src/utils/token.ts (1)

1-21: Token 工具函数实现正确

实现简洁且具有 SSR 兼容性。所有函数都正确处理了服务端渲染场景(检查 window 是否存在)。

ZaeXT/frontend/src/features/categories/CategoryNode.vue (1)

1-41: 组件实现正确

分类节点组件实现清晰,递归渲染逻辑正确,样式支持深色模式。

ZaeXT/frontend/src/features/chat/ChatMessageList.vue (1)

1-56: 消息列表组件实现正确

消息渲染、样式和自动滚动功能实现良好。支持流式和错误状态显示,用户体验设计合理。

ZaeXT/frontend/src/stores/model.ts (3)

11-15: 未知 tier 的降级行为需要明确

Line 14 中,当 tier 不在 tierOrder 中时,返回 tierOrder.length,这意味着未知的 tier 被视为最高级别(最不受限)。这可能不是预期行为——通常应该将未知 tier 视为最低级别(最受限)以确保安全。

建议验证此行为是否符合业务逻辑:

const getTierRank = (tier: string | undefined) => {
  if (!tier) return 0
  const index = tierOrder.indexOf(tier)
  // 当前:未知 tier 返回最高级别 (tierOrder.length)
  // 建议:未知 tier 返回最低级别 (0)?
  return index === -1 ? tierOrder.length : index
}

36-47: 空结果可能阻止重试

Line 37 的守卫条件 fetched.value && models.value.length > 0 意味着如果 API 返回空数组(合法响应),fetched 会被设为 truemodels.length 为 0,后续调用仍会尝试重新获取。但如果服务器确实没有模型,这会导致不必要的重复请求。

当前实现可能是有意为之(支持延迟加载场景),但值得确认:

如果空数组是有效结果且不应重试,可以简化守卫条件:

-if (loading.value || (fetched.value && models.value.length > 0)) return
+if (loading.value || fetched.value) return

24-34: 模型过滤逻辑实现正确

基于用户 tier 的访问控制实现清晰,正确区分了可用模型和锁定模型。

ZaeXT/frontend/src/router/index.ts (1)

75-81: 整体路由配置与滚动行为设置清晰可读。

创建 Router、历史模式与滚动到顶部的默认行为合理。

ZaeXT/frontend/src/views/profile/ProfileView.vue (1)

119-127: 保存流程与并发保护合理,LGTM。

hasChanges 判断 + saving 闸门有效,保存后通过 store 更新并由 watch 重置草稿,行为正确。

ZaeXT/internal/handler/router.go (1)

41-46: 路由注册完善,包含会话标题更新与分类更新,LGTM。

此前缺失的 PUT /conversations/:id/title 已加入,资源组织清晰。

ZaeXT/frontend/src/features/chat/ChatComposer.vue (2)

94-116: 发送流程健壮性良好,异常回滚输入,LGTM。

校验空白、模型可用性检查、流式中禁用输入与停止按钮切换都到位。


62-73: 请确认模型列表的加载时机,避免初次进入时无可用模型。

若未在更上层(如 Workspace)调用 modelStore 的加载逻辑,建议在组件挂载时触发一次 ensureLoaded()(若有)或说明数据依赖。

是否在应用启动或进入 Chat 页面时已确保加载模型列表?

ZaeXT/frontend/src/api/sse.ts (1)

41-43: 代码实现正确,无需修改。

已验证:

  • README 明确指出 VITE_API_BASE 默认值为 http://localhost:8080/api/v1
  • 文档要求 VITE_API_BASE 指向 /api/v1 根路径
  • SSE 文件和 http.ts 对 VITE_API_BASE 的使用方式完全一致
  • 无配置不一致问题

设置正确的 VITE_API_BASE(包含 /api/v1)后,SSE 端点路由正确。

ZaeXT/frontend/src/api/conversations.ts (1)

12-47: 审查意见基于对 HTTP 方法签名的不正确假设,建议不适用。

经验证,http.ts 直接导出标准 axios 实例及其绑定方法,未使用自定义泛型包装。标准 axios 方法签名仅接受 1 个泛型参数(响应类型),而非审查意见所假设的多个泛型参数:

  • get<T>(url, config?) — 仅 1 个泛型
  • post<T>(url, data?, config?) — 仅 1 个泛型
  • put<T>(url, data?, config?) — 仅 1 个泛型
  • delete<T>(url, config?) — 仅 1 个泛型

代码中现有的 http.get<Type[], Type[]>http.post<Type, Type> 写法虽然看起来冗余,但这是因为 TypeScript 允许向泛型函数传递多于所需的类型参数(多余参数被忽略)。此处传递的第二个泛型对 axios 没有类型约束效果。

审查意见关于"纠正第二个泛型为请求体类型"的建议基于错误的 API 假设,不会达到预期的类型安全提升。建议开发者忽略此审查意见。

Likely an incorrect or invalid review comment.

ZaeXT/frontend/src/api/http.ts (1)

1-118: HTTP 客户端实现完善

整体实现质量很高:

  • ApiError 类设计合理,统一了错误处理格式
  • 请求拦截器正确注入 Authorization 头
  • 响应拦截器妥善处理了各类 HTTP 状态码(401/403/404/5xx)
  • 与路由和通知系统集成良好
ZaeXT/frontend/src/types/conversation.ts (1)

1-57: 类型定义清晰完整

类型定义覆盖了对话系统的核心数据结构,包括消息角色、对话摘要、消息详情以及各类请求载荷,类型声明准确且完整。

ZaeXT/internal/service/chat_service.go (3)

108-112: 已正确添加模型列表边界检查

代码现在在访问 freeModels[0] 之前正确检查了切片是否为空,避免了 panic 风险。修复良好!


250-252: 通道关闭模式修复正确

现在使用 defer close() 统一管理通道关闭,移除了手动关闭调用,避免了二次关闭和泄漏风险。


377-396: 自动标题生成已修复为完整聚合

代码现在正确地遍历所有流式响应块并累积完整标题,避免了之前只读取首块导致的截断问题。

ZaeXT/frontend/package.json (4)

53-58: ESLint/Prettier工具链配置良好

工具链完整地配置了ESLint、Prettier、TypeScript支持以及import排序,符合现代Vue 3项目的最佳实践。特别是包含了eslint-config-prettier来解决ESLint和Prettier的冲突。


14-16: 测试配置完整性良好

包含Vitest、Vue Test Utils、覆盖率工具和API模拟工具,提供了完整的测试框架。test:watch脚本支持开发期间的实时测试,coverage脚本支持测试覆盖率分析。


67-67: TailwindCSS工具链配置完整

包含TailwindCSS、PostCSS、autoprefixer和Stylelint,支持现代化的样式开发和质量控制。@tailwindcss/forms和@tailwindcss/typography插件提供了表单和文章排版的优化样式。


1-5: 项目基础配置合理

项目名称、私有标记、版本号和ESM模块类型配置都合理。ESM模块类型与现代Vue 3和Vite的推荐保持一致。

"@headlessui/vue": "^1.7.22",
"@heroicons/vue": "^2.1.5",
"@vueuse/core": "^10.11.0",
"axios": "^1.7.7",
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

验证axios版本安全性

axios 1.7.7是相对较旧的版本。建议检查是否存在已知的安全漏洞并考虑升级到最新的稳定版本。


🌐 Web query:

axios latest version and security advisories

💡 Result:

As of Oct 27, 2025:

Recommended action: upgrade to the latest axios (>=1.12.2) and audit transitive dependencies (e.g., form-data) per the GitHub advisories. [1][2][3]

Sources:
[1] axios releases — GitHub.
[2] axios security / advisories — GitHub.
[3] CVE-2025-27152 advisory (Aqua / IBM summary).


升级axios以修复关键安全漏洞

axios 1.7.7 受 CVE-2025-27152 SSRF和凭证泄露漏洞影响,该漏洞已在1.8.2及之后版本修复。建议升级至最新稳定版本 axios v1.12.2 或至少升级到1.8.2,并审计依赖项(如form-data)中的其他安全建议。

    "axios": "^1.12.2",
🤖 Prompt for AI Agents
In ZaeXT/frontend/package.json around line 22 the project pins "axios": "^1.7.7"
which is vulnerable (CVE-2025-27152); update the dependency to a patched version
(preferably "axios": "^1.12.2" or at minimum "^1.8.2") in package.json, then
reinstall to update the lockfile (npm/yarn install), run npm audit / yarn audit
to surface any remaining issues and specifically inspect and upgrade transitive
packages such as form-data if flagged, and run the test suite to confirm nothing
breaks.

Comment on lines +32 to +49
```
frontend/
├─ public/
├─ src/
│ ├─ api/ # axios 实例、REST 封装、SSE 工具
│ ├─ components/ # 全局组件(Markdown 渲染、空态、设置面板等)
│ ├─ features/ # 领域组件(auth / chat / models / categories / recycle-bin)
│ ├─ views/ # 页面级视图,对应路由
│ ├─ stores/ # Pinia stores(auth / conversations / models / categories / recycleBin)
│ ├─ router/ # 路由定义、守卫、nprogress
│ ├─ utils/ # jwt、fetch、通知、时间格式化、i18n 助手
│ ├─ i18n/ # 语言资源与配置
│ ├─ styles/ # Tailwind 入口与全局样式
│ └─ types/ # DTO、领域模型、响应类型
├─ .env.example # 前端环境变量模版
├─ vitest.config.ts # 测试配置
└─ 各类工程配置 # Vite / Tailwind / ESLint / Stylelint / Prettier / tsconfig
```
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 | 🟡 Minor

指定代码块语言标记。

代码块缺少语言标记,应按 markdownlint MD040 规则补充。目录结构块建议标记为 bashplaintext

-```
+```bash
 frontend/
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

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

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
ZaeXT/frontend/README.md around lines 32 to 49: the fenced code block showing
the frontend directory tree is missing a language tag which violates
markdownlint MD040; update the opening fence to include a language such as bash
or plaintext (for example change ``` to ```bash) so the block is correctly
marked, and ensure the closing fence remains ``` to produce a valid fenced code
block.

Comment on lines +60 to +65
if (!response.ok || !response.body) {
const error = new Error(`Failed to open stream: ${response.status} ${response.statusText}`)
onError?.(error)
throw 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

onError 可能被重复触发(一次在分支内,一次在 catch)。

!response.ok 分支已调用 onError 后再次抛出,随后 catch 又调用一次。请确保只触发一次。

-    if (!response.ok || !response.body) {
-      const error = new Error(`Failed to open stream: ${response.status} ${response.statusText}`)
-      onError?.(error)
-      throw error
-    }
+    if (!response.ok || !response.body) {
+      const error = new Error(`Failed to open stream: ${response.status} ${response.statusText}`)
+      // 仅返回错误,不再额外抛出;或抛出但不在 catch 中二次回调
+      onError?.(error)
+      return controller
+    }

同时在最外层 catch 中避免再次调用 onError(见下方合并 Diff)。

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

🤖 Prompt for AI Agents
In ZaeXT/frontend/src/api/sse.ts around lines 60 to 65, the code currently calls
onError inside the !response.ok branch and then throws the error which causes
the outer catch to call onError again; remove the onError call from inside the
branch and just throw the Error (or return/handle) so the outer catch is the
single place that invokes onError, and in the top-level catch ensure you invoke
onError exactly once (guard with a flag or only call it there) and avoid calling
onError twice for the same failure.

Comment on lines +107 to +114
const pump = async () => {
while (!abortController.signal.aborted) {
const { value, done } = await reader.read()
if (done) {
onDone()
break
}
buffer += decoder.decode(value, { stream: true })
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

onDone 可能被调用两次(收到 [DONE] 与 reader 结束)。

processChunk 收到 [DONE] 会调用 onDone() 并返回,但 pump 后续因流关闭会再调用一次。建议用布尔位进行幂等保护,并在收到 DONE 后中止读取。

+    let finished = false
@@
-    const processChunk = (chunk: string) => {
+    const processChunk = (chunk: string) => {
       const lines = chunk.split('\n')
       for (const line of lines) {
@@
-        if (data === DONE) {
-          onDone()
-          return
-        }
+        if (data === DONE) {
+          if (!finished) {
+            finished = true
+            onDone()
+            abortController.abort()
+          }
+          return
+        }
@@
-        const { value, done } = await reader.read()
+        const { value, done } = await reader.read()
         if (done) {
-          onDone()
+          if (!finished) {
+            finished = true
+            onDone()
+          }
           break
         }

Also applies to: 88-97

🤖 Prompt for AI Agents
In ZaeXT/frontend/src/api/sse.ts around lines 107-114 (and also apply the same
fix to 88-97), onDone() can be invoked twice (once when processChunk sees
"[DONE]" and again when reader signals done); add an idempotency guard boolean
(e.g., doneCalled = false) and wrap every onDone() call with if (!doneCalled) {
doneCalled = true; onDone(); }; when you detect "[DONE]" also immediately stop
further reading by calling abortController.abort() (or cancel the reader) so the
pump loop exits without calling onDone() again.

Comment on lines +119 to +125
if (Date.now() - lastHeartbeat > HEARTBEAT_INTERVAL) {
const heartbeatError = new Error('SSE heartbeat timeout')
emitToast({ type: 'warning', message: '连接中断,正在尝试恢复...' })
onError?.(heartbeatError)
break
}
}
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

心跳超时仅 break 未取消请求,可能导致连接泄漏或悬挂。

在心跳超时分支应 abortController.abort(),并(若需要)触发重连逻辑;当前提示“尝试恢复”但未真正重试。

-        if (Date.now() - lastHeartbeat > HEARTBEAT_INTERVAL) {
+        if (Date.now() - lastHeartbeat > HEARTBEAT_INTERVAL) {
           const heartbeatError = new Error('SSE heartbeat timeout')
           emitToast({ type: 'warning', message: '连接中断,正在尝试恢复...' })
           onError?.(heartbeatError)
-          break
+          abortController.abort()
+          // TODO: 若需自动重试,可在此返回标志并由上层发起重连
+          break
         }

同时可考虑将文案改为“连接中断”,避免误导用户认为会自动重试。

📝 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 (Date.now() - lastHeartbeat > HEARTBEAT_INTERVAL) {
const heartbeatError = new Error('SSE heartbeat timeout')
emitToast({ type: 'warning', message: '连接中断,正在尝试恢复...' })
onError?.(heartbeatError)
break
}
}
if (Date.now() - lastHeartbeat > HEARTBEAT_INTERVAL) {
const heartbeatError = new Error('SSE heartbeat timeout')
emitToast({ type: 'warning', message: '连接中断,正在尝试恢复...' })
onError?.(heartbeatError)
abortController.abort()
// TODO: 若需自动重试,可在此返回标志并由上层发起重连
break
}
🤖 Prompt for AI Agents
In ZaeXT/frontend/src/api/sse.ts around lines 119 to 125, the SSE heartbeat
timeout branch only breaks the loop but does not cancel the in-flight request or
actually reconnect; call abortController.abort() to cancel the fetch/stream,
replace the toast message with a non-misleading "连接中断" (or similar), and invoke
the existing reconnect handler or emit a reconnect event / call a reconnect
function after aborting so the UI message matches behavior.

Comment on lines +96 to +102
if (token && to.meta.requiresAuth) {
const authStore = useAuthStore()
const { profile, loading } = storeToRefs(authStore)
if (!profile.value && !loading.value) {
await authStore.fetchProfileSafely()
}
}
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

受保护路由在 Token 失效时可能放行,建议在获取资料失败后回退到登录。

若 token 无效/过期,fetchProfileSafely() 会吞掉异常并将 initialized=true,但此处没有后续处理,用户可能进入需要鉴权的页面且 profile 仍为 null。建议在初始化后仍无 profile 时,重定向到登录并附带 redirect

可按下述方式补齐处理:

   if (token && to.meta.requiresAuth) {
     const authStore = useAuthStore()
     const { profile, loading } = storeToRefs(authStore)
     if (!profile.value && !loading.value) {
-      await authStore.fetchProfileSafely()
+      await authStore.fetchProfileSafely()
+      if (!profile.value) {
+        return next({ name: 'login', query: { redirect: to.fullPath } })
+      }
     }
   }

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

🤖 Prompt for AI Agents
In ZaeXT/frontend/src/router/index.ts around lines 96 to 102, the guard calls
await authStore.fetchProfileSafely() but does not handle the case where the
token is invalid and profile remains null; update the logic so that after
awaiting fetchProfileSafely() you check again if profile.value is falsy and, if
so, stop the current navigation and redirect the user to the login route
(include the original destination as a redirect/query param, e.g.
redirect=to.fullPath) so protected routes are not accessible with an invalid
token.

Comment on lines +291 to +302
onError(error) {
console.error('[conversations] streaming error', error)
if (streamingState.pendingAssistantId) {
updateMessage(conversationId, streamingState.pendingAssistantId, (msg) => {
msg.status = 'error'
})
}
emitToast({ type: 'error', message: '生成失败,请重试。' })
streamingState.controller = null
streamingState.pendingAssistantId = null
streamingState.isStreaming = false
},
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

中止流时被误判为错误;结束/中止后未清理全部状态。

  • 当前 onError 无论是否人为 abort() 都将消息标记为 error,与预期的「中止不是错误」不符。
  • onDone/abortStreaming 未重置 enableThinking/modelId,可能导致 UI 残留状态。

建议按下述 diff 处理:

 const abortStreaming = () => {
   if (streamingState.controller) {
     streamingState.controller.abort()
   }
   if (streamingState.pendingAssistantId && currentId.value) {
     updateMessage(currentId.value, streamingState.pendingAssistantId, (msg) => {
       msg.status = 'done'
     })
   }
   streamingState.controller = null
   streamingState.pendingAssistantId = null
   streamingState.isStreaming = false
+  streamingState.enableThinking = false
+  streamingState.modelId = undefined
 }

 ...
         onDone() {
           if (!streamingState.pendingAssistantId) return
           updateMessage(conversationId, streamingState.pendingAssistantId, (msg) => {
             msg.status = 'done'
           })
           streamingState.controller = null
           streamingState.pendingAssistantId = null
           streamingState.isStreaming = false
+          streamingState.enableThinking = false
+          streamingState.modelId = undefined
           loadConversations()
         },
         onError(error) {
-          console.error('[conversations] streaming error', error)
-          if (streamingState.pendingAssistantId) {
-            updateMessage(conversationId, streamingState.pendingAssistantId, (msg) => {
-              msg.status = 'error'
-            })
-          }
-          emitToast({ type: 'error', message: '生成失败,请重试。' })
-          streamingState.controller = null
-          streamingState.pendingAssistantId = null
-          streamingState.isStreaming = false
+          // 中止不视为错误
+          if ((error as DOMException)?.name === 'AbortError') {
+            streamingState.controller = null
+            streamingState.pendingAssistantId = null
+            streamingState.isStreaming = false
+            streamingState.enableThinking = false
+            streamingState.modelId = undefined
+            return
+          }
+          console.error('[conversations] streaming error', error)
+          if (streamingState.pendingAssistantId) {
+            updateMessage(conversationId, streamingState.pendingAssistantId, (msg) => {
+              msg.status = 'error'
+            })
+          }
+          emitToast({ type: 'error', message: '生成失败,请重试。' })
+          streamingState.controller = null
+          streamingState.pendingAssistantId = null
+          streamingState.isStreaming = false
+          streamingState.enableThinking = false
+          streamingState.modelId = undefined
         },

Also applies to: 226-238, 281-289

Comment on lines +26 to +30
const restoreItem = async (id: number) => {
await restoreConversation(id)
emitToast({ type: 'success', message: '对话已恢复。' })
await loadItems()
}
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

建议使用国际化而非硬编码文本。

第28行的成功提示消息使用了硬编码的中文字符串 '对话已恢复。'。为了与项目中其他部分保持一致(如 LoginView、RegisterView 等都使用了 i18n),建议使用国际化方案。

可以考虑:

  1. 在 i18n 配置中添加相应的键值
  2. 通过依赖注入或组合式函数获取 t 函数
  3. 使用 t('recycleBin.restored') 替代硬编码字符串

相同的问题也存在于第34行的 deletePermanently 方法中。

🤖 Prompt for AI Agents
In ZaeXT/frontend/src/stores/recycle-bin.ts around lines 26 to 34, the success
messages are hardcoded Chinese strings (line 28 '对话已恢复。' and line 34 in
deletePermanently) — replace these with i18n keys: add appropriate entries like
recycleBin.restored and recycleBin.deleted in the i18n resource files, obtain
the translation function (e.g., via useI18n or by injecting t into this module),
and call t('recycleBin.restored') and t('recycleBin.deleted') in place of the
hardcoded strings so the toast messages use the localization system.

Comment on lines +5 to +32
const config: Config = {
darkMode: 'class',
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
theme: {
extend: {
colors: {
brand: {
DEFAULT: '#2563eb',
light: '#60a5fa',
dark: '#1d4ed8',
},
surface: {
DEFAULT: '#111827',
light: '#f8fafc',
dark: '#0f172a',
},
},
fontFamily: {
sans: ['Inter', 'ui-sans-serif', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'ui-monospace', 'SFMono-Regular', 'monospace'],
},
boxShadow: {
elevated: '0 10px 30px -15px rgb(37 99 235 / 0.5)',
},
},
},
plugins: [forms(), typography()],
}
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

Tailwind 配置完整且合理!

配置文件结构清晰,自定义主题(品牌色、字体、阴影)定义得当。使用 class 模式的深色模式提供了更好的控制。插件集成也很合适。

建议验证自定义字体文件是否已包含在项目中或通过 CDN 加载:


🏁 Script executed:

#!/bin/bash
# 搜索字体文件或 CDN 引用
rg -n "Inter|JetBrains Mono" ZaeXT/frontend/index.html ZaeXT/frontend/src --type html --type css

Length of output: 0


🏁 Script executed:

#!/bin/bash
# 搜索更广范围的字体引用
echo "=== 搜索所有HTML和CSS文件中的字体引用 ==="
rg -n -i "Inter|JetBrains Mono|fonts|@import" ZaeXT/frontend --type html --type css

echo ""
echo "=== 检查项目根目录结构 ==="
find ZaeXT/frontend -maxdepth 2 -type f -name "*.html" -o -name "index.*"

echo ""
echo "=== 搜索package.json中的字体依赖 ==="
cat ZaeXT/frontend/package.json 2>/dev/null | grep -i "font" || echo "未找到字体相关依赖"

echo ""
echo "=== 检查是否有公共字体文件 ==="
find ZaeXT/frontend -type d -name "fonts" -o -type d -name "assets"

echo ""
echo "=== 搜索所有HTML文件中的link标签 ==="
rg -n "<link" ZaeXT/frontend --type html

Length of output: 346


🏁 Script executed:

#!/bin/bash
# 首先探索项目结构
echo "=== 列出根目录 ==="
ls -la

echo ""
echo "=== 查找tailwind.config.ts文件 ==="
find . -name "tailwind.config.ts" -type f

echo ""
echo "=== 查找项目中的所有package.json ==="
find . -maxdepth 3 -name "package.json" -type f

echo ""
echo "=== 查找所有index.html ==="
find . -maxdepth 4 -name "index.html" -type f

Length of output: 1708


🏁 Script executed:

#!/bin/bash
# 检查字体配置
echo "=== 检查ZaeXT/frontend/index.html中的字体引用 ==="
cat ./ZaeXT/frontend/index.html

echo ""
echo "=== 检查ZaeXT/frontend/package.json ==="
cat ./ZaeXT/frontend/package.json

echo ""
echo "=== 搜索src目录中的字体引用 ==="
find ./ZaeXT/frontend/src -type f \( -name "*.css" -o -name "*.scss" -o -name "*.ts" -o -name "*.tsx" -o -name "*.vue" -o -name "*.js" \) | head -20

echo ""
echo "=== 搜索Inter和JetBrains Mono字体引用 ==="
rg -n "Inter|JetBrains Mono|@font-face|googleapis|fonts\." ./ZaeXT/frontend --type html --type css --type ts --type js

echo ""
echo "=== 检查是否存在fonts目录 ==="
find ./ZaeXT/frontend -type d -name "fonts" -o -type d -name "assets" -o -type d -name "public"

Length of output: 4292


字体配置不完整,需要添加字体加载

Tailwind配置中定义的 InterJetBrains Mono 字体在项目中尚未加载。浏览器将回退至系统字体。需要通过以下方式之一添加字体:

  1. 使用 Google Fonts CDN(推荐):在 index.html<head> 中添加:

    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
  2. 或使用本地字体文件:下载字体文件到 public/fonts/ 并在CSS中使用 @font-face 声明

  3. 或通过npm包:安装 typeface-intertypeface-jetbrains-mono 并在主入口文件导入

Comment on lines +10 to +13
script: {
defineModel: true,
propsDestructure: true,
},
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

确认使用实验性功能

propsDestructure: true 是 Vue 的实验性功能,在生产环境中可能存在稳定性风险。请确认团队了解此功能的状态并愿意承担潜在的破坏性变更。

可以通过以下方式确认 Vue 版本是否支持该功能:


🌐 Web query:

Vue 3 propsDestructure feature stability and version support

💡 Result:

Short answer: Props destructuring in <script setup> started as an experimental feature in Vue 3.3 and was stabilized (enabled by default) in Vue 3.5. You can opt out via the compiler option (propsDestructure: false or 'error'), and older bundler/plugin configs (e.g. @vitejs/plugin-vue) required explicit opt-in or changed the config shape across versions. [1][2][3]

Details & important dates

  • Introduced as an experimental feature in Vue 3.3 (RFC / opt-in). [1]
  • Stabilized and enabled by default in Vue 3.5 (no longer needs propsDestructure: true; can be disabled). Announcement: Sept 2024. [2]
  • If using Vite/@vitejs/plugin-vue, note config differences across plugin versions (propsDestructure moved from script.props to script.features.propsDestructure in @vitejs/plugin-vue v5.1.0+). [3]

Sources

  • Vue 3.3 experimental Reactive Props Destructure. [1]
  • Vue 3.5 release notes / announcement (stabilized, enabled by default). [2]
  • @vitejs/plugin-vue config change note (example & migration). [3]


等待执行结果来继续分析。

---

🏁 Script executed:

```shell
#!/bin/bash
# Check package.json for Vue and @vitejs/plugin-vue versions
if [ -f "ZaeXT/frontend/package.json" ]; then
  echo "=== ZaeXT/frontend/package.json ===" 
  cat ZaeXT/frontend/package.json | grep -A 20 '"dependencies"\|"devDependencies"' | grep -E '"vue"|"@vitejs/plugin-vue"'
else
  echo "File not found at ZaeXT/frontend/package.json"
  # Try to find it
  find . -name "package.json" -path "*/frontend/*" 2>/dev/null | head -5
fi

Length of output: 157



---

🏁 Script executed:

```shell
#!/bin/bash
# Check package.json with correct path
cat ./git/ZaeXT/frontend/package.json | jq '.dependencies, .devDependencies' | grep -E '"vue"|"@vitejs/plugin-vue"'

Length of output: 65



---

🏁 Script executed:

```shell
#!/bin/bash
# Verify current state
echo "=== Current directory ==="
pwd

echo ""
echo "=== Check if vite.config.ts exists ==="
fd -e ts -e js "vite.config" 2>/dev/null | head -5

echo ""
echo "=== Search for package.json near vite.config.ts ==="
fd "vite.config.ts" -x dirname {} \; 2>/dev/null | head -1 | xargs -I {} find {} -name "package.json" 2>/dev/null

Length of output: 234



---

🏁 Script executed:

```shell
#!/bin/bash
# Extract Vue and @vitejs/plugin-vue versions
cd /home/jailuser/git

echo "=== Vue and @vitejs/plugin-vue versions ==="
cat ZaeXT/frontend/package.json | grep -E '"vue"|"@vitejs/plugin-vue"' | head -10

Length of output: 101



---

🏁 Script executed:

```shell
#!/bin/bash
# View the full vite.config.ts to understand context
cat ZaeXT/frontend/vite.config.ts

Length of output: 1814


修改 Vite 配置中的 propsDestructure 选项位置

propsDestructure 在 Vue 3.5 已成为稳定功能,不再是实验性功能。但是,@vitejs/plugin-vue v5.1.0+ 要求将 propsDestructure 选项从 script 对象移到 features 对象中。

当前配置需要更新为:

vue({
  features: {
    defineModel: true,
    propsDestructure: true,
  },
})
🤖 Prompt for AI Agents
In ZaeXT/frontend/vite.config.ts around lines 10 to 13, the Vite Vue plugin
configuration places propsDestructure under script; move both defineModel and
propsDestructure into a features object passed to the vue() plugin instead of
script (i.e., remove propsDestructure from script and add features: {
defineModel: true, propsDestructure: true }), ensuring the plugin import/usage
remains correct and no duplicate keys exist.

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