Skip to content

Conversation

@eryajf
Copy link
Member

@eryajf eryajf commented Jan 1, 2026

Summary by CodeRabbit

发布说明

  • 新功能

    • 添加了数据库支持的配置管理功能,支持大语言模型、云账号、集成平台等多维度配置
    • 实现了对话管理和聊天历史追踪功能
    • 新增MCP工具集成和状态管理能力
    • 支持DingTalk、Feishu、WeCom等多平台服务管理
    • 新增仪表板,展示系统统计信息和健康状态
  • 认证与安全

    • 实现基于JWT的身份验证系统
    • 添加用户管理和权限控制
  • 文档

    • 提供完整的API设计文档和实现指南

✏️ Tip: You can customize this high-level summary in your review settings.

- 添加 ListEnabledTools 方法,根据数据库配置过滤已启用的工具
- 支持服务器级别和工具级别的启用/禁用控制
- 保持向后兼容,数据库查询失败时返回所有工具

✨ feat(llm): 更新 MCP 工具获取逻辑,仅使用已启用的工具

- 将 getMCPTools 方法中的 ListTools 替换为 ListEnabledTools
- 更新相关注释和日志,明确说明只返回启用的工具

✨ feat(server): 重构聊天处理器,集成 LLM 客户端和 MCP 工具支持

- 重写 Completions 方法,使用新的 LLM 客户端进行对话
- 支持流式和非流式响应,自动集成已启用的 MCP 工具
- 重构响应格式,符合 OpenAI API 规范
- 添加聊天路由注册逻辑,确保在 MCP 服务器初始化后注册
- 实现动态加载 LLM 配置,替换原有的静态模型列表
- 增加错误处理,当数据库查询失败时返回 500 错误
- 仅返回启用的模型,并标记与配置匹配的默认模型
@eryajf eryajf added the enhancement New feature or request label Jan 1, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 1, 2026

总体分析

Walkthrough

本PR实现了ZenOps系统的全栈配置数据库化改造。引入SQLite数据库、JWT认证、聊天日志持久化、对话管理、MCP服务器/工具数据库管理,并通过新的HTTP API层提供完整的配置、认证、聊天和服务管理功能。前端构建集成到编译流程中。

Changes

内聚类 / 文件(s) 变更说明
工作流和构建配置
.github/workflows/docker.yml, .github/workflows/release.yml, Makefile
添加Node.js/前端构建步骤、Go环境和UPX安装;集成前端编译到Docker和发布流程;新增build-webbuild-alldev目标
忽略规则和文档
.gitignore, README.md, docs/*
添加数据库文件和临时目录忽略;新增10份详细文档覆盖API设计、配置迁移、前端实现、验证清单等
依赖管理
go.mod, config.example.yaml
添加SQLite、GORM、JWT依赖;移除dingtalk.agent_id配置字段
数据库访问层
internal/database/db.go, internal/database/migrate.go
实现GORM+SQLite单例数据库连接、自动迁移、默认Admin用户创建
数据模型
internal/model/user.go, internal/model/chat_log.go, internal/model/conversation.go, internal/model/config_*.go, internal/model/mcp_*.go, internal/model/common.go
新增10+个GORM模型:User(含密码哈希)、ChatLog、Conversation、LLMConfig、ProviderAccount、IMConfig、CICDConfig、MCPServer、MCPTool、MCPLog、SystemConfig;支持JSON序列化和数据库映射
认证中间件和服务
internal/middleware/auth.go, internal/server/auth_handler.go
实现JWT生成/解析、Bearer令牌验证中间件、登录/登出/用户信息/修改密码端点
配置服务层
internal/service/config_service.go, internal/service/config_migration.go, internal/service/mcp_migration.go
提供CRUD操作的ConfigService;实现YAML→SQLite迁移;支持MCP服务器JSON配置迁移和导出
聊天和对话服务
internal/service/chat_log_service.go, internal/service/conversation_service.go
ChatLogService管理消息持久化(用户消息、AI响应、消息线程);ConversationService管理对话生命周期
MCP日志服务
internal/service/mcp_log_service.go
MCPLogService记录MCP工具调用(请求/响应/延迟/状态),支持筛选和统计
HTTP API处理器
internal/server/config_handler.go, internal/server/chat_handler.go, internal/server/conversation_handler.go, internal/server/dashboard_handler.go, internal/server/log_handler.go, internal/server/mcp_handler.go, internal/server/history_handler.go, internal/server/service_handler.go
完整的REST API:配置管理(LLM/提供商/IM/CICD/MCP)、聊天补全(流式SSE)、对话CRUD、仪表板统计、日志查询、历史管理、服务切换
服务编排
internal/server/service_manager.go, internal/server/http.go, internal/server/version_handler.go
ServiceManager统一管理DingTalk/Feishu/Wecom生命周期;HTTPGinServer注册完整路由和静态文件服务;VersionHandler暴露版本信息
IM平台集成
internal/server/dingtalk.go, internal/server/dingtalk_stream_handler.go, internal/feishu/handler.go, internal/wecom/handler.go, cmd/root.go
增强IM处理器:配置优先级(DB优先→YAML降级)、消息持久化(源跟踪)、AI响应保存;支持来自DB的MCP服务器初始化
MCP客户端增强
internal/mcpclient/manager.go, internal/imcp/server.go
Manager添加RegisterFromDBIsRegisteredUnregister方法;MCPServer添加ListEnabledTools方法;LLM客户端支持启用工具筛选和流式工具调用
LLM客户端扩展
internal/llm/client.go, internal/llm/openai.go
添加ChatWithToolsAndStreamChatWithToolsAndStreamMessages支持历史消息和流式工具执行;增强工具调用日志记录(包含延迟、服务器名、工具名);集成MCP日志服务
配置加载器
internal/config/db_loader.go, internal/config/loader.go
实现数据库配置加载优先级(DB→YAML→迁移);可插拔dbLoader和dbMigrator钩子
前端资源嵌入
web/embed.go, zenops-web
添加嵌入式文件系统支持(//go:embed dist/*);更新前端子模块到新版本
示例文档
docs/INTEGRATION_EXAMPLE.go.example
展示完整的服务初始化和优雅关闭模式

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI/Root
    participant DB as SQLite DB
    participant Config as ConfigService
    participant MCP as MCPManager
    participant HTTP as HTTP Server
    
    CLI->>CLI: 初始化数据库连接
    CLI->>DB: AutoMigrate (建表/默认用户)
    CLI->>Config: SetDBLoader/SetDBMigrator
    Config->>DB: LoadConfigFromDB
    alt DB配置存在
        Config-->>CLI: ✓ 返回DB配置
    else DB配置缺失
        Config->>Config: loadConfigFromYAML
        Config->>DB: migrateConfigToDB
        Config-->>CLI: ✓ 返回YAML配置
    end
    
    CLI->>MCP: SetGlobalMCPManager
    CLI->>MCP: InitializeMCPServersFromDB
    MCP->>DB: 查询启用的MCP服务器
    MCP->>MCP: RegisterFromDB (规范化/启动)
    
    CLI->>HTTP: SetMCPServer
    CLI->>HTTP: registerChatRoutes
    HTTP->>HTTP: 注册版本/认证/配置/对话/日志端点
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰 数据库来临,YAML已成往事,
SQLite承载配置梦,
JWT守护每道门,
聊天流淌数据库间,
工具在MCP中轻舞,
全栈改造,焕然一新!✨

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive 标题描述不够具体。'增加了web界面的简单交互能力'过于宽泛,未能清晰反映此PR的主要变更内容。实际上本PR引入了数据库配置管理、MCP工具集成、聊天日志持久化等多个重要功能,仅用'简单交互能力'难以准确概括。 建议将标题改为更具体的描述,例如:'feat: 集成数据库配置管理和MCP工具功能'或'feat: 实现配置数据库化和前端管理界面',以更准确反映这是一次全栈功能集成。
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings

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

@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: 3

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
internal/dingtalk/handler.go (1)

360-391: 潜在的 goroutine 泄漏:for-select 循环未处理 context 取消。

ctx 被取消时(如请求超时或客户端断开),当前的 for-select 循环不会退出,可能导致 goroutine 泄漏。建议添加 ctx.Done() 分支处理。

🔎 建议的修复方案
 	for {
 		select {
+		case <-ctx.Done():
+			// 上下文取消,清理并退出
+			if updateBuffer != "" {
+				fullContent += updateBuffer
+			}
+			fullContent += "\n\n---\n⚠️ 请求已取消"
+			_ = h.client.UpdateAIStreamCard(trackID, fullContent, true)
+			return
 		case content, ok := <-responseCh:
 			if !ok {
 				// 流结束,发送最终更新
internal/server/http.go (1)

131-146: CORS 配置允许所有来源

当前 CORS 配置使用 Access-Control-Allow-Origin: *,这在生产环境中可能存在安全风险。建议在非调试模式下配置特定的允许来源。

🔎 建议的改进
 func (s *HTTPGinServer) corsMiddleware() gin.HandlerFunc {
 	return func(c *gin.Context) {
-		c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
+		// 生产环境应配置具体的允许域名
+		origin := c.GetHeader("Origin")
+		if s.config.Server.HTTP.Debug || isAllowedOrigin(origin) {
+			c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
+		}
 		c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
🟠 Major comments (18)
internal/model/config_provider.go-48-58 (1)

48-58: 安全风险:敏感凭证字段直接暴露在 JSON 响应中。

AccessKeySecretKey 字段带有 json 标签,会在 API 响应中直接返回明文凭证。建议:

  1. SecretKey 使用 json:"-" 或自定义序列化隐藏敏感信息
  2. 存储时考虑加密处理
🔎 建议的修复方案
 type ProviderAccount struct {
 	ID        uint        `gorm:"primaryKey" json:"id"`
 	Provider  string      `gorm:"size:50;not null;index:idx_provider_name" json:"provider"`
 	Name      string      `gorm:"size:100;not null;index:idx_provider_name" json:"name"`
 	Enabled   bool        `gorm:"default:true" json:"enabled"`
-	AccessKey string      `gorm:"type:text;not null" json:"access_key"`
-	SecretKey string      `gorm:"type:text;not null" json:"secret_key"`
+	AccessKey string      `gorm:"type:text;not null" json:"access_key,omitempty"`
+	SecretKey string      `gorm:"type:text;not null" json:"-"` // 不在响应中返回
 	Regions   StringArray `gorm:"type:text" json:"regions"`
 	CreatedAt time.Time   `json:"created_at"`
 	UpdatedAt time.Time   `json:"updated_at"`
 }
internal/config/loader.go-38-50 (1)

38-50: 占位函数模式存在设计问题。

当前的占位函数实现存在以下问题:

问题分析:

  1. loadConfigFromDBmigrateConfigToDB 函数总是返回错误
  2. 注释表明"实际加载逻辑在 cmd/root.go 中实现",但代码中直接调用这些函数
  3. 函数依赖外部初始化,但这种依赖关系不明确
  4. 如果外部初始化失败,代码会静默地回退到 YAML 模式

设计改进建议:
使用依赖注入或策略模式,使配置加载逻辑更加清晰和可测试:

// ConfigLoader 配置加载器接口
type ConfigLoader interface {
    LoadFromDB() (*Config, error)
    MigrateToDB(*Config) error
}

// 使用默认加载器(占位实现)
var defaultLoader ConfigLoader = &noopLoader{}

type noopLoader struct{}

func (n *noopLoader) LoadFromDB() (*Config, error) {
    return nil, fmt.Errorf("database loading not configured")
}

func (n *noopLoader) MigrateToDB(cfg *Config) error {
    return fmt.Errorf("database migration not configured")
}

// SetConfigLoader 允许外部设置实际的加载器实现
func SetConfigLoader(loader ConfigLoader) {
    defaultLoader = loader
}

// LoadConfig 中使用
func LoadConfig(configFile string) (*Config, error) {
    dbConfig, err := defaultLoader.LoadFromDB()
    // ...
    if err := defaultLoader.MigrateToDB(yamlConfig); err != nil {
    // ...
}

这种设计具有以下优势:

  • 明确的依赖关系和接口契约
  • 更好的可测试性(可以注入 mock 实现)
  • 清晰的初始化流程
  • 避免隐式的外部依赖

基于学习内容,相关的 internal/config/db_loader.go 文件可能已经引入了 ConfigLoader 抽象,建议检查是否可以直接使用该抽象,而不是使用占位函数。

internal/mcpclient/manager.go-191-194 (1)

191-194: StreamableHttp 客户端使用 1 秒固定等待

与 SSE 客户端相同的问题,且等待时间更长。这里也应该改用更可靠的同步机制。

internal/mcpclient/manager.go-165-168 (1)

165-168: 避免使用固定的 time.Sleep 等待连接建立

使用 time.Sleep(500 * time.Millisecond) 等待 SSE 连接建立是不可靠的。在高负载或网络延迟情况下,500ms 可能不够;而在正常情况下又是不必要的等待。建议使用带超时的同步等待机制或轮询检查连接状态。

🔎 建议改进方向

考虑以下方案之一:

  1. 使用 MCP 客户端库提供的同步 Start 方法(如果有)
  2. 实现轮询检查连接状态的逻辑,带超时退出
  3. 如果必须使用 sleep,至少应该是可配置的,并在日志中说明
-	// Start() 是异步的,等待一小段时间让 SSE 连接建立
-	time.Sleep(500 * time.Millisecond)
+	// 等待连接建立,使用可配置的超时
+	connectionTimeout := time.Duration(cfg.Timeout) * time.Second / 10
+	if connectionTimeout < 500*time.Millisecond {
+		connectionTimeout = 500 * time.Millisecond
+	}
+	time.Sleep(connectionTimeout)

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

docs/VERIFICATION_CHECKLIST.md-369-379 (1)

369-379: 注意敏感数据加密需求

文档在第374行明确指出"数据库中密钥未加密(需要后续添加加密)"。这是一个重要的安全风险。建议创建跟踪issue来确保在生产环境使用前实现密钥加密功能。

需要验证是否已有相关issue跟踪此安全改进任务。如果需要,我可以帮助生成加密/解密的实现方案。

internal/database/migrate.go-86-107 (1)

86-107: 默认密码的安全性考虑

代码使用硬编码的弱密码 "admin123" 作为默认管理员密码,并在日志中明文输出(第105行)。虽然这在开发环境可以接受,但存在以下风险:

  1. 如果在生产环境部署时未修改默认密码,会造成严重的安全隐患
  2. 日志文件可能被其他系统收集,导致密码泄露

建议添加首次启动时强制修改密码的流程,或通过环境变量配置初始密码。

如需要,我可以帮助设计一个更安全的初始化方案,例如生成随机密码或要求管理员在首次登录时设置密码。

internal/server/mcp_handler.go-47-49 (1)

47-49: 核心功能尚未实现

TODO注释表明实际的MCP工具调用逻辑尚未实现,目前只返回占位符数据。这意味着DebugExecute端点不能正常工作。

建议在合并前完成实际的MCP集成,或者明确标记此端点为"实验性"或"开发中"状态。

如需要,我可以帮助设计MCP Client Manager的集成方案或生成相应的实现代码。

internal/server/mcp_handler.go-69-70 (1)

69-70: 避免未使用导入的不当做法

第69-70行通过赋值给空白标识符来避免unused import错误,这是一个代码异味。正确的做法是:

  1. 如果确实需要这些导入,应该在实际功能中使用它们
  2. 如果当前不需要,应该移除这些导入
🔎 建议修复

方案1:如果ctx确实应该被使用(推荐)

-	_ = ctx        // 使用 ctx
-	_ = uuid.New() // 避免 unused import 错误
+	// ctx已在超时控制中使用
+	// uuid将在实际MCP调用实现时使用

方案2:如果当前不需要这些导入

 import (
-	"context"
 	"net/http"
 	"time"
 
 	"cnb.cool/zhiqiangwang/pkg/logx"
 	"github.com/eryajf/zenops/internal/service"
 	"github.com/gin-gonic/gin"
-	"github.com/google/uuid"
 )

并移除第69-70行的workaround代码。

internal/database/migrate.go-188-204 (1)

188-204: 在事务中执行迁移以确保数据安全

第189行的 DROP TABLE IF EXISTS llm_config 及后续的 AutoMigrate 和数据插入(第200行)未在事务中执行。当前代码在 initDB() 中直接调用 AutoMigrate(db) 而无显式事务包装。若插入操作失败,旧表已被删除,导致数据丢失且无法回滚。

建议将 migrateLLMConfig 函数中的关键操作包装在事务中,确保原子性:

if err := db.Transaction(func(tx *gorm.DB) error {
    // 执行 DROP, CREATE, INSERT 操作
    // ...
}).Error; err != nil {
    return err
}
internal/server/dingtalk.go-303-306 (1)

303-306: Context 传递到 goroutine 存在风险

将 HTTP 请求的 ctx 直接传递给 goroutine 可能导致问题:当 HTTP 响应返回后,请求 context 可能被取消,导致异步处理中的 MCP 调用、数据库操作等被中断。

建议使用独立的 background context,类似 WeCom handler 中的做法(见 internal/wecom/handler.go 第 91 行)。

🔎 建议修复
 	// 异步处理查询
-	go h.processQueryAsync(ctx, msg, intent, username, source, userLog)
+	go h.processQueryAsync(context.Background(), msg, intent, username, source, userLog)
internal/server/chat_handler.go-158-159 (1)

158-159: 应使用请求上下文而非 Background

使用 context.Background() 会导致请求取消时 LLM 调用无法中止。建议使用 c.Request.Context() 以支持请求取消和超时。

🔎 建议的修复
 	// 使用 llm.Client 调用 LLM(支持 MCP 工具)
-	ctx := context.Background()
+	ctx := c.Request.Context()
internal/service/conversation_service.go-75-83 (1)

75-83: 删除操作应使用事务

删除会话时先删除消息再删除会话,但未使用事务包装。如果第二步失败,会导致消息已删除但会话仍存在的不一致状态。

🔎 建议的修复
 // DeleteConversation 删除会话(软删除)
 func (s *ConversationService) DeleteConversation(id uint) error {
-	// 删除会话下的所有消息
-	if err := s.db.Where("conversation_id = ?", id).Delete(&model.ChatLog{}).Error; err != nil {
-		return err
-	}
-	// 删除会话
-	return s.db.Delete(&model.Conversation{}, id).Error
+	return s.db.Transaction(func(tx *gorm.DB) error {
+		// 删除会话下的所有消息
+		if err := tx.Where("conversation_id = ?", id).Delete(&model.ChatLog{}).Error; err != nil {
+			return err
+		}
+		// 删除会话
+		return tx.Delete(&model.Conversation{}, id).Error
+	})
 }
internal/server/config_handler.go-438-450 (1)

438-450: API 响应中返回了敏感凭证信息(AccessKey/SecretKey)

CreateProviderAccountUpdateProviderAccount 在响应中返回完整的 aksk 字段。这些是敏感的云服务凭证,不应在 API 响应中明文返回。

建议对敏感字段进行脱敏处理(如只显示最后几位)或完全不返回。

🔎 建议的修复方案
+// maskSecret 对敏感信息进行脱敏
+func maskSecret(s string) string {
+	if len(s) <= 4 {
+		return "****"
+	}
+	return "****" + s[len(s)-4:]
+}
+
 c.JSON(http.StatusOK, Response{
 	Code:    200,
 	Message: "Provider account created successfully",
 	Data: gin.H{
 		"id":      account.ID,
 		"name":    account.Name,
 		"enabled": account.Enabled,
-		"ak":      account.AccessKey,
-		"sk":      account.SecretKey,
+		"ak":      maskSecret(account.AccessKey),
+		"sk":      maskSecret(account.SecretKey),
 		"regions": account.Regions,
 	},
 })

Also applies to: 483-495

internal/server/service_manager.go-184-195 (1)

184-195: Feishu 服务存在相同的竞态条件问题

与 DingTalk 相同,feishuRunning 在 goroutine 启动之前就被设置为 true(第 193 行)。

internal/service/config_migration.go-390-396 (1)

390-396: DingTalk 配置加载时 AppSecret 被设置为与 AppKey 相同的值

第 393 行 AppSecret: dingtalk.AppKey 意味着 AppSecret 和 AppKey 使用相同的值。这可能不是预期的行为,结合迁移代码(第 155-156 行)来看,似乎 AppKey 字段存储的实际上是 AppSecret

建议统一数据模型命名或添加清晰的注释说明字段的实际用途。

internal/service/config_migration.go-197-207 (1)

197-207: Wecom 配置使用 AgentID 字段存储 AES Key 是语义混乱的

第 201 行的注释 // 临时存储 AES Key 表明这是一个临时方案。使用 AgentID 字段存储 EncodingAESKey 会导致代码难以理解和维护。

建议在 model.IMConfig 中添加专门的字段来存储不同 IM 平台的特定配置,或使用 JSON 字段存储扩展属性。

internal/server/service_manager.go-96-108 (1)

96-108: 竞态条件:在 goroutine 确认启动成功之前就设置了 dingtalkRunning = true

dingtalkRunning 在第 105 行被设置为 true,但此时 goroutine 中的 dingtalkService.Start() 可能还未执行或可能失败。这会导致 GetServiceStatus() 返回错误的状态。

建议在 goroutine 内部确认启动成功后再更新状态,或使用 channel/sync 原语来同步。

🔎 建议的修复方案
+	// 使用 channel 同步启动状态
+	startedCh := make(chan error, 1)
+
 	// 启动服务
 	go func() {
+		startedCh <- nil // 发送启动信号
 		if err := dingtalkService.Start(serviceCtx); err != nil {
 			logx.Error("DingTalk service error: %v", err)
 			sm.mu.Lock()
 			sm.dingtalkRunning = false
 			sm.mu.Unlock()
 		}
 	}()
 
+	// 等待启动信号或使用更可靠的启动确认机制
 	sm.dingtalkRunning = true

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

internal/server/config_handler.go-21-27 (1)

21-27: 全局 MCP 管理器的懒加载存在竞态条件

GetGlobalMCPManager 在多个 goroutine 同时调用时可能创建多个 Manager 实例。第 23-25 行的检查和赋值不是原子操作。

建议使用 sync.Once 确保只初始化一次。

🔎 建议的修复方案
+import "sync"
+
 // 全局 MCP 客户端管理器
 var globalMCPManager *mcpclient.Manager
+var mcpManagerOnce sync.Once
 
 // GetGlobalMCPManager 获取全局 MCP 客户端管理器
 func GetGlobalMCPManager() *mcpclient.Manager {
-	if globalMCPManager == nil {
-		globalMCPManager = mcpclient.NewManager()
-	}
+	mcpManagerOnce.Do(func() {
+		if globalMCPManager == nil {
+			globalMCPManager = mcpclient.NewManager()
+		}
+	})
 	return globalMCPManager
 }

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

🟡 Minor comments (19)
docs/API_DESIGN.md-16-17 (1)

16-17: 修复 Markdown 代码块缺失语言标识

根据 markdownlint 提示,第 16-17 行和第 21-22 行的代码块缺少语言标识符,建议添加以改善渲染效果和可读性。

🔎 建议的修复
 ### Base URL
-```
+```text
 /api/v1

Content-Type

- +text
application/json

Also applies to: 21-22

internal/service/mcp_migration.go-132-147 (1)

132-147: 导出时非字符串类型的环境变量和 Headers 会被静默丢弃。

类型断言 v.(string) 失败时,对应的键值对不会被写入导出文件,可能导致数据丢失。建议添加日志警告或使用 fmt.Sprintf 转换。

🔎 建议的修复方案
 		env := make(map[string]string)
 		for k, v := range server.Env {
 			if str, ok := v.(string); ok {
 				env[k] = str
+			} else if v != nil {
+				env[k] = fmt.Sprintf("%v", v)
 			}
 		}
internal/config/db_loader.go-22-36 (1)

22-36: 并发安全问题:setter 函数存在数据竞争风险。

SetDBLoaderSetDBMigratordefaultLoader 的读写操作不是原子的。如果在程序初始化阶段并发调用这些函数,可能导致数据竞争。

🔎 建议使用 sync.Once 或互斥锁
+import "sync"
+
+var (
+	defaultLoader *DefaultConfigLoader
+	loaderOnce    sync.Once
+)
+
+func initLoader() {
+	loaderOnce.Do(func() {
+		defaultLoader = &DefaultConfigLoader{}
+	})
+}
+
 func SetDBLoader(loader func() (*Config, error)) {
-	if defaultLoader == nil {
-		defaultLoader = &DefaultConfigLoader{}
-	}
+	initLoader()
 	defaultLoader.dbLoader = loader
 }

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

internal/dingtalk/handler.go-18-27 (1)

18-27: chatLogService 字段已添加但未在此文件中使用或初始化。

该字段在结构体中声明(第26行),但:

  • 已注释的 NewMessageHandler 构造函数(第29-70行)并未初始化此字段
  • internal/dingtalk/handler.go 中的任何方法都未使用该字段

而其他类似的处理器(wecom/handler.gofeishu/handler.goserver/dingtalk.go 等)广泛使用 chatLogService 来持久化聊天日志。建议补充初始化逻辑和相应的使用代码,或移除该字段以保持一致性。

docs/FINAL_IMPLEMENTATION_SUMMARY.md-6-6 (1)

6-6: 确认实现日期是否正确

文档中的实现时间显示为 2025-12-21,但当前日期是 2026-01-01。如果这是指过去的实现日期,建议修正为 2024-12-21

README.md-34-34 (1)

34-34: 补充图片的替代文本以提升无障碍性

图片缺少 alt 属性,建议添加描述性文本以提升页面的可访问性,便于屏幕阅读器用户理解内容。

🔎 建议的修复
-![](https://t.eryajf.net/imgs/2025/12/1766657550093.webp)
+![前端页面开发进度截图](https://t.eryajf.net/imgs/2025/12/1766657550093.webp)

基于静态分析工具 markdownlint 的提示。

Makefile-31-50 (1)

31-50: 构建目标实现合理,建议确认包管理器一致性

新增的前端构建目标结构清晰:

  • build-web: 构建前端并复制产物
  • build-all: 完整的前后端构建
  • dev: 一键构建并启动服务

需要注意的问题(与 release.yml 相同):

  1. 包管理器一致性: Makefile 使用 npm,但文档提到 pnpm,请统一工具选择
  2. 错误处理: dev 目标在构建失败时仍会尝试运行,建议添加错误检查
🔎 可选的改进建议

如果前端确实使用 pnpm:

 build-web:
 	@echo ">>> 构建前端..."
-	cd ${WEB_DIR} && npm install && npm run build
+	cd ${WEB_DIR} && pnpm install && pnpm run build

增强 dev 目标的错误处理:

 dev: build-all
 	@echo ">>> 启动服务..."
-	./$(BINARY_NAME) run
+	@if [ -f ./$(BINARY_NAME) ]; then \
+		./$(BINARY_NAME) run; \
+	else \
+		echo "错误: 构建失败,未找到可执行文件"; \
+		exit 1; \
+	fi
internal/server/history_handler.go-61-68 (1)

61-68: 数据库操作缺少错误处理

log_handler.go 相同的问题:CountFind 操作的错误未被检查。

🔎 建议的修改
 	// 计算总数
 	var total int64
-	query.Count(&total)
+	if err := query.Count(&total).Error; err != nil {
+		c.JSON(http.StatusInternalServerError, Response{
+			Code:    500,
+			Message: "查询总数失败: " + err.Error(),
+		})
+		return
+	}

 	// 分页查询
 	var logs []model.ChatLog
 	offset := (page - 1) * pageSize
-	query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&logs)
+	if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&logs).Error; err != nil {
+		c.JSON(http.StatusInternalServerError, Response{
+			Code:    500,
+			Message: "查询对话记录失败: " + err.Error(),
+		})
+		return
+	}
internal/server/log_handler.go-74-81 (1)

74-81: 数据库操作缺少错误处理

CountFind 操作可能失败,但错误未被检查和处理。这可能导致返回错误数据而客户端无法察觉。

🔎 建议的修改
 	// 计算总数
 	var total int64
-	query.Count(&total)
+	if err := query.Count(&total).Error; err != nil {
+		c.JSON(http.StatusInternalServerError, Response{
+			Code:    500,
+			Message: "查询总数失败: " + err.Error(),
+		})
+		return
+	}

 	// 分页查询
 	var logs []model.MCPLog
 	offset := (page - 1) * pageSize
-	query.Order("timestamp DESC").Offset(offset).Limit(pageSize).Find(&logs)
+	if err := query.Order("timestamp DESC").Offset(offset).Limit(pageSize).Find(&logs).Error; err != nil {
+		c.JSON(http.StatusInternalServerError, Response{
+			Code:    500,
+			Message: "查询日志失败: " + err.Error(),
+		})
+		return
+	}
internal/server/history_handler.go-114-116 (1)

114-116: 子消息查询缺少错误处理

获取子消息时未检查数据库错误。

🔎 建议的修改
 	// 获取子消息
 	var children []model.ChatLog
-	db.Where("parent_content = ?", current.ID).Find(&children)
+	if err := db.Where("parent_content = ?", current.ID).Find(&children).Error; err != nil {
+		c.JSON(http.StatusInternalServerError, Response{
+			Code:    500,
+			Message: "查询子消息失败: " + err.Error(),
+		})
+		return
+	}
internal/server/dashboard_handler.go-52-54 (1)

52-54: AVG 查询未处理 NULL 值

mcp_logs 表为空时,AVG(latency) 返回 NULL,直接 Scan 到 float64 可能导致问题或得到 0 值。建议使用 COALESCE 处理,与 mcp_log_service.go 中的实现保持一致。

🔎 建议修复
 	// 计算平均延迟
 	var avgLatency float64
-	db.Model(&model.MCPLog{}).Select("AVG(latency)").Row().Scan(&avgLatency)
+	db.Model(&model.MCPLog{}).Select("COALESCE(AVG(latency), 0)").Row().Scan(&avgLatency)
internal/server/auth_handler.go-90-96 (1)

90-96: 令牌生成错误信息可能泄露实现细节

middleware.GenerateToken 的错误信息直接返回给客户端,可能泄露内部实现细节。建议返回通用错误信息。

🔎 建议修复
 	// 生成 JWT Token
 	token, err := middleware.GenerateToken(user.ID, user.Username, user.Roles)
 	if err != nil {
+		logx.Error("Failed to generate token: %v", err)
 		c.JSON(http.StatusInternalServerError, Response{
 			Code:    500,
-			Message: "生成令牌失败: " + err.Error(),
+			Message: "服务器内部错误,请稍后重试",
 		})
 		return
 	}
internal/server/version_handler.go-23-32 (1)

23-32: 全局变量初始化存在竞态条件

globalVersionHandler 的初始化和 GetVersionInfo 函数中的 nil 检查不是线程安全的。如果多个请求同时到达且未初始化,可能会创建多个实例。

建议使用 sync.Once 确保线程安全。

🔎 建议修复
+import "sync"
+
+var (
+	globalVersionHandler *VersionHandler
+	versionOnce          sync.Once
+)
+
 // InitVersionHandler 初始化全局版本处理器
 func InitVersionHandler(version, gitCommit, buildTime string) {
-	globalVersionHandler = &VersionHandler{
-		version:   version,
-		gitCommit: gitCommit,
-		buildTime: buildTime,
-	}
+	versionOnce.Do(func() {
+		globalVersionHandler = &VersionHandler{
+			version:   version,
+			gitCommit: gitCommit,
+			buildTime: buildTime,
+		}
+	})
 }

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

internal/server/chat_handler.go-407-413 (1)

407-413: 同样的 UTF-8 截断问题

此处的字符串截断存在与上方相同的问题,应使用 rune 切片处理。

🔎 建议的修复
 	// 如果生成的标题为空,使用默认值
 	if generatedTitle == "" {
-		if len(userMessage) > 10 {
-			return userMessage[:10] + "..."
+		runes := []rune(userMessage)
+		if len(runes) > 10 {
+			return string(runes[:10]) + "..."
 		}
 		return userMessage
 	}
internal/server/chat_handler.go-381-386 (1)

381-386: 字符串截断可能破坏 UTF-8 字符

len(userMessage) 返回字节数而非字符数,对中文等多字节字符进行截断可能产生无效的 UTF-8 序列。

🔎 建议的修复
 	if err != nil {
 		logx.Error("Failed to generate conversation title: %v", err)
 		// 如果生成失败,使用用户消息的前10个字符作为标题
-		if len(userMessage) > 10 {
-			return userMessage[:10] + "..."
+		runes := []rune(userMessage)
+		if len(runes) > 10 {
+			return string(runes[:10]) + "..."
 		}
 		return userMessage
 	}
internal/server/conversation_handler.go-217-241 (1)

217-241: DeleteConversation 缺少存在性检查

删除前未检查会话是否存在。如果 ID 不存在,服务层可能返回成功(GORM 的 Delete 对不存在的记录不报错),这可能导致前端收到误导性的成功响应。

🔎 建议的改进
 func (h *ConversationHandler) DeleteConversation(c *gin.Context) {
 	idStr := c.Param("id")
 	id, err := strconv.ParseUint(idStr, 10, 32)
 	if err != nil {
 		c.JSON(http.StatusBadRequest, Response{
 			Code:    400,
 			Message: "Invalid conversation ID",
 		})
 		return
 	}

+	// 检查会话是否存在
+	conversation, err := h.conversationService.GetConversation(uint(id))
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, Response{
+			Code:    500,
+			Message: "Failed to check conversation: " + err.Error(),
+		})
+		return
+	}
+	if conversation == nil {
+		c.JSON(http.StatusNotFound, Response{
+			Code:    404,
+			Message: "Conversation not found",
+		})
+		return
+	}
+
 	if err := h.conversationService.DeleteConversation(uint(id)); err != nil {

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

internal/server/config_handler.go-1685-1689 (1)

1685-1689: 响应码不一致:GetAllConfig 使用 Code: 0,而其他接口使用 Code: 200

第 1686 行使用 Code: 0,而文件中其他所有成功响应都使用 Code: 200。这种不一致可能导致前端处理混乱。

🔎 建议的修复方案
 c.JSON(http.StatusOK, Response{
-	Code:    0,
+	Code:    200,
 	Message: "success",
 	Data:    config,
 })
internal/service/config_service.go-122-130 (1)

122-130: GetProviderAccount 未处理记录不存在的情况,与其他方法不一致

大多数 Get 方法(如 GetLLMConfigGetIMConfig)在记录不存在时返回 (nil, nil),但 GetProviderAccount 直接返回错误。这种不一致可能导致调用方处理逻辑混乱。

🔎 建议的修复方案
 func (s *ConfigService) GetProviderAccount(id uint) (*model.ProviderAccount, error) {
 	var account model.ProviderAccount
 	err := s.db.First(&account, id).Error
 	if err != nil {
+		if err == gorm.ErrRecordNotFound {
+			return nil, nil
+		}
 		return nil, err
 	}
 	return &account, nil
 }
internal/service/config_migration.go-131-137 (1)

131-137: 使用字符串比较检测 GORM 错误是脆弱的做法

第 132 行使用 err.Error() == "record not found" 来检测记录不存在。这种方式依赖于错误消息的具体文本,容易因 GORM 版本更新而失效。

应该使用 errors.Is(err, gorm.ErrRecordNotFound) 进行比较。

🔎 建议的修复方案
+import "errors"
+
 func (s *ConfigService) GetProviderAccountByName(provider, name string) (*model.ProviderAccount, error) {
 	var account model.ProviderAccount
 	err := s.db.Where("provider = ? AND name = ?", provider, name).First(&account).Error
 	if err != nil {
-		if err.Error() == "record not found" {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
 			return nil, nil
 		}
 		return nil, err
 	}
 	return &account, nil
 }

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

🧹 Nitpick comments (65)
docs/CONFIG_MIGRATION_SUMMARY.md (2)

71-71: 修复代码块缺失语言标识符的问题。

Line 71 和 101 的代码块缺少语言标识符,影响文档渲染和语法高亮。

🔎 建议的修复

Line 71 的代码块应该添加语言标识符:

-```
+```text
 /api/v1/config/

Line 101 的代码块应该添加语言标识符:

-```
+```text
 ZenOps/

根据静态分析提示。

Also applies to: 101-101


212-246: 将硬制表符替换为空格。

Lines 212-246 的代码示例中使用了硬制表符(Tab),应该使用空格来保持一致性和兼容性。

🔎 建议的修复

将所有硬制表符替换为空格(建议使用2个或4个空格进行缩进):

 import (
-	"github.com/eryajf/ZenOps/internal/config"
-	"github.com/eryajf/ZenOps/internal/database"
-	"github.com/eryajf/ZenOps/internal/service"
+  "github.com/eryajf/ZenOps/internal/config"
+  "github.com/eryajf/ZenOps/internal/database"
+  "github.com/eryajf/ZenOps/internal/service"
 )

应用到整个代码块的所有缩进。

根据静态分析提示。

docs/CONFIG_DATABASE_MIGRATION.md (2)

37-103: 将硬制表符替换为空格。

Lines 37-103 的 Go 代码示例中使用了硬制表符,应该使用空格来保持一致性。

🔎 建议的修复

将代码块中的所有硬制表符替换为空格(Go 惯例使用 Tab,但在 Markdown 中建议使用空格):

 import (
-	"log"
-	"os"
+  "log"
+  "os"
 
-	"github.com/eryajf/ZenOps/internal/config"
-	"github.com/eryajf/ZenOps/internal/database"
-	"github.com/eryajf/ZenOps/internal/service"
-	"github.com/spf13/cobra"
+  "github.com/eryajf/ZenOps/internal/config"
+  "github.com/eryajf/ZenOps/internal/database"
+  "github.com/eryajf/ZenOps/internal/service"
+  "github.com/spf13/cobra"
 )

应用到整个代码块。

根据静态分析提示。


273-273: 添加代码块语言标识符。

Line 273 的代码块缺少语言标识符,建议添加 text 标识符以改善文档渲染。

🔎 建议的修复
-```
+```text
 数据库配置 > YAML 配置 > 环境变量 > 默认值

</details>

根据静态分析提示。

</blockquote></details>
<details>
<summary>internal/model/user.go (2)</summary><blockquote>

`32-39`: **建议在哈希前验证密码强度**

当前实现会接受并哈希任何密码,包括空字符串。建议在调用 `bcrypt.GenerateFromPassword` 之前添加密码强度验证(最小长度、复杂度要求等),以增强安全性。



<details>
<summary>🔎 建议的改进方案</summary>

```diff
 // SetPassword 设置密码(加密)
 func (u *User) SetPassword(password string) error {
+	// 验证密码强度
+	if len(password) < 8 {
+		return fmt.Errorf("password must be at least 8 characters")
+	}
+	if password == "" {
+		return fmt.Errorf("password cannot be empty")
+	}
+	
 	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
 	if err != nil {
 		return err
 	}
 	u.Password = string(hashedPassword)
 	return nil
 }

17-23: 考虑改进 Email 和 Roles 字段设计

  1. Email 字段:如果 Email 用于登录或需要保证唯一性,建议添加 uniqueIndex 约束。
  2. Roles 字段:使用逗号分隔的字符串存储角色会限制查询灵活性(如按角色过滤用户)。建议考虑使用 JSON 数组或建立多对多关系表。
🔎 改进建议
-	Email    string `gorm:"size:100" json:"email"`
+	Email    string `gorm:"size:100;uniqueIndex" json:"email"`
-	Roles    string `gorm:"size:255;default:'user'" json:"roles"` // 角色列表,逗号分隔
+	Roles    []string `gorm:"serializer:json;default:'[\"user\"]'" json:"roles"` // 角色列表,JSON数组

或者使用多对多关系:

type User struct {
    // ... 其他字段
    Roles []*Role `gorm:"many2many:user_roles;" json:"roles"`
}

type Role struct {
    ID   uint   `gorm:"primarykey"`
    Name string `gorm:"uniqueIndex;not null"`
}
web/embed.go (1)

1-24: 实现正确,建议补充构建文档

嵌入前端资源的实现正确。//go:embed 在编译时要求 dist/ 目录存在,建议在项目文档或 README 中说明前端构建是后端编译的前置步骤。

internal/model/common.go (1)

14-44: Scan 方法的错误处理可以优化

当前实现在第 31 行总是初始化 map,即使后续的 JSON 反序列化可能失败。这意味着如果 JSON 无效,*jm 会被设置为空 map 而非保持原值。建议在反序列化成功后再赋值,避免部分状态。

🔎 改进建议
 func (jm *JSONMap) Scan(value interface{}) error {
 	if value == nil {
 		*jm = make(map[string]interface{})
 		return nil
 	}
 
 	var bytes []byte
 	switch v := value.(type) {
 	case []byte:
 		bytes = v
 	case string:
 		bytes = []byte(v)
 	default:
 		return fmt.Errorf("failed to unmarshal JSONMap value: %v", value)
 	}
 
-	// 初始化 map
-	*jm = make(map[string]interface{})
-
 	// 如果是空字符串或空JSON对象,直接返回
 	if len(bytes) == 0 || string(bytes) == "{}" || string(bytes) == "null" {
+		*jm = make(map[string]interface{})
 		return nil
 	}
 
 	// 尝试反序列化
+	temp := make(map[string]interface{})
-	if err := json.Unmarshal(bytes, jm); err != nil {
+	if err := json.Unmarshal(bytes, &temp); err != nil {
 		return fmt.Errorf("failed to unmarshal JSONMap: %w", err)
 	}
+	*jm = temp
 
 	return nil
 }
internal/config/db_loader.go (2)

7-18: 接口声明但未实现。

ConfigLoader 接口已定义,但 DefaultConfigLoader 并未实现该接口的方法。如果接口是为将来扩展预留的,建议添加注释说明;否则应实现接口方法或移除未使用的接口。


38-66: loader.go 中的 LoadConfig 存在代码重复。

根据提供的代码片段,internal/config/loader.go 中已有类似的 LoadConfig 函数实现相同的逻辑(先尝试数据库加载,失败后从 YAML 加载,然后尝试迁移)。建议统一配置加载入口,避免维护两套相似逻辑。

internal/dingtalk/handler.go (1)

29-70: 建议移除大段注释代码。

被注释掉的 NewMessageHandler 函数占用了约 40 行代码。如果不再需要,建议删除;如果是临时禁用,建议添加说明注释或使用 TODO 标记。

docs/FINAL_IMPLEMENTATION_SUMMARY.md (2)

63-76: 可选:为代码块添加语言标识符

建议为 API 端点列表的代码块添加语言标识符(如 textyaml),以提升文档的规范性。

🔎 建议的改进
-```
+```text
 /api/v1/config/
 ├── /llm                [GET, PUT]
 ...
</details>

基于 markdownlint 静态分析提示。

---

`220-220`: **可选:使用 Markdown 链接语法格式化 URL**

文档中多处使用了裸 URL,建议使用标准的 Markdown 链接语法以提升可读性和语义化。



例如:
```diff
-访问: http://localhost:3006
+访问: [http://localhost:3006](http://localhost:3006)

基于 markdownlint 静态分析提示。

Also applies to: 313-316, 374-374, 381-381, 388-388

internal/model/mcp_log.go (1)

6-21: 模型设计合理,可选改进建议

MCPLog 模型的字段设计和索引策略整体合理:

  • ✅ 时间戳、服务名、工具名等关键字段已添加索引
  • ✅ 使用 TEXT 类型存储可能较大的 JSON 数据
  • ✅ 状态字段大小限制合理

可选的改进建议:

  1. 考虑为 ChatLogID 添加外键约束,确保引用完整性
  2. 如果预期 Source 字段可能包含更长的标识符,可将 size:50 调整为 size:100
🔎 可选的外键约束示例
-	ChatLogID    uint      `json:"chat_log_id" gorm:"index"` // 关联的对话记录ID(如果是从LLM调用)
+	ChatLogID    uint      `json:"chat_log_id" gorm:"index;constraint:OnDelete:SET NULL"` // 关联的对话记录ID(如果是从LLM调用)

注:是否添加外键约束取决于项目的数据完整性策略。

Makefile (1)

87-89: 可选:扩展 clean 目标以清理前端构建产物

当前 clean 目标仅清理后端的 bin 目录,建议也清理前端构建产物 web/dist,以确保完整的清理操作。

🔎 建议的改进
 .PHONY: clean
 clean:
 	@rm -rf ${OUTPUT_DIR}
+	@rm -rf web/dist
+	@echo ">>> 清理完成"

或者添加独立的 clean-web 目标:

+.PHONY: clean-web
+clean-web:
+	@rm -rf web/dist
+	@echo ">>> 前端构建产物已清理"
+
 .PHONY: clean
 clean:
 	@rm -rf ${OUTPUT_DIR}
.github/workflows/release.yml (1)

20-25: 为前端仓库访问权限添加文档说明

工作流中使用 GITHUB_TOKEN 从独立仓库 eryajf/zenops-web 检出前端代码。建议:

  1. 在项目文档(如 README 或 CONTRIBUTING)中明确说明 eryajf/zenops-web 仓库的访问要求(公开/私有)
  2. 如果 zenops-web 是私有仓库,文档应说明在本地环境中所需的令牌权限或使用专用的个人访问令牌(PAT)替代 GITHUB_TOKEN
  3. 在工作流中添加注释,说明此令牌的用途和权限要求

当前实现在 IMPLEMENTATION_SUMMARY.md 中有记录,但在用户设置本地开发或贡献代码时缺乏明确指导。

docs/FRONTEND_CLEANUP_SUMMARY.md (1)

155-177: 建议为代码块指定语言标识。

第 155 行的代码块缺少语言标识符,建议添加以提高可读性和一致性。

🔎 建议的修复
-```
+```text
 web/src/views/
 ├── auth/                    # 认证页面
 │   ├── login/
docs/FRONTEND_IMPLEMENTATION.md (4)

9-32: 建议为代码块指定语言标识。

第 9 行的代码块缺少语言标识符,建议添加以提高可读性。

🔎 建议的修复
-```
+```text
 web/src/
 ├── api/

175-182: 建议为代码块指定语言标识。

第 175 行的代码块缺少语言标识符,建议添加以提高可读性。

🔎 建议的修复
-```
+```text
 /config                    # 配置管理
 ├── /llm                  # LLM 配置

271-274: 建议格式化 URL 链接。

第 271-274 行的裸 URL 建议使用 Markdown 链接格式,提高文档可读性。

🔎 建议的修复
 启动后访问:
-- http://localhost:3006/config/llm - LLM 配置
-- http://localhost:3006/config/provider - 云厂商账号
-- http://localhost:3006/config/integration - 集成配置
-- http://localhost:3006/config/mcp - MCP Server
+- [LLM 配置](http://localhost:3006/config/llm)
+- [云厂商账号](http://localhost:3006/config/provider)
+- [集成配置](http://localhost:3006/config/integration)
+- [MCP Server](http://localhost:3006/config/mcp)

279-286: 建议为代码块指定语言标识。

第 279 行的代码块建议指定语言标识符。

🔎 建议的修复
-```
+```text
 前端: http://localhost:3006/api/v1/config/*
  ↓
internal/llm/client.go (3)

260-274: 工具名称解析逻辑可能不够健壮。

当前逻辑假设外部 MCP 工具前缀必须包含连字符 -(如 aliyun-ack),但内置工具如 search_ecs_by_ip 也包含下划线。如果外部工具前缀不含连字符,会被误判为内置工具。

建议考虑从数据库或工具注册表中获取准确的 server/tool 映射,而非依赖字符串解析。


276-293: 每次工具调用都实例化 MCPLogService 可能影响性能。

NewMCPLogService() 在每次 executeToolCall 时被调用。虽然该服务内部使用 database.GetDB() 单例,但频繁创建服务实例仍是不必要的开销。

🔎 建议将服务实例注入 Client 或使用单例模式
 type Client struct {
 	config    *Config
 	mcpServer MCPServer
+	mcpLogService *service.MCPLogService
 }

 func NewClient(config *Config, mcpServer MCPServer) *Client {
 	return &Client{
 		config:    config,
 		mcpServer: mcpServer,
+		mcpLogService: service.NewMCPLogService(),
 	}
 }

然后在 executeToolCall 中使用 c.mcpLogService


281-282: 用户名硬编码为 "llm",缺少上下文传递。

注释中已提到"用户信息需要从上下文传递",建议后续通过 context.Context 携带用户信息,以便准确记录实际触发工具调用的用户。

是否需要我帮助设计上下文传递用户信息的方案?

internal/database/db.go (1)

83-95: Close() 存在潜在的竞态条件。

Close() 检查的是包级别的 db 变量,但如果在 GetDB() 调用之前调用 Close()db 仍为 nil。此外,如果 GetDB()Close() 并发执行,可能导致问题。

🔎 建议使用互斥锁保护
 var (
 	db   *gorm.DB
 	once sync.Once
+	mu   sync.RWMutex
 )

 func Close() error {
+	mu.Lock()
+	defer mu.Unlock()
 	if db == nil {
 		return nil
 	}
 	// ...
 }
internal/model/config_im.go (1)

7-18: 敏感字段 AppKey 建议考虑加密存储。

AppKey 字段存储应用密钥等敏感信息,当前以明文存储在数据库中。建议后续考虑使用加密存储或 secrets manager 来保护这些凭证。

模型结构本身设计合理,GORM 标签配置正确。

internal/model/mcp_tool.go (1)

6-15: JSON 字段命名不一致。

IsEnabled 字段的 JSON 标签使用驼峰命名 "isEnabled",而其他字段使用蛇形命名(如 "server_id""created_at")。建议统一命名风格以保持 API 一致性。

🔎 建议统一为蛇形命名
-	IsEnabled   bool                   `json:"isEnabled" gorm:"default:true"`
+	IsEnabled   bool                   `json:"is_enabled" gorm:"default:true"`

同时需要更新前端和其他消费端的字段名。

internal/model/chat_log.go (1)

6-17: 模型结构整体良好,但字段命名可改进

ParentContent 字段实际存储的是父消息的 ID,但命名暗示存储的是内容。建议重命名为 ParentIDParentMessageID 以提高可读性。

🔎 建议的修改
-	ParentContent  uint       `json:"parent_content"`             // 父消息ID
+	ParentID       uint       `json:"parent_id"`                  // 父消息ID
docs/IMPLEMENTATION_SUMMARY.md (1)

95-96: 将裸 URL 格式化为 Markdown 链接

根据静态分析提示,裸 URL 应格式化为 Markdown 链接以符合规范。

🔎 建议的修改
-前端: http://localhost:3000
-后端: http://localhost:8080/api/v1
+前端: <http://localhost:3000>
+后端: <http://localhost:8080/api/v1>
internal/server/user_handler.go (1)

36-47: 登录请求缺少必填字段验证

当前仅检查 JSON 绑定错误,但不验证 usernamepassword 是否为空。建议添加基本验证。

🔎 建议的修改
 	if err := c.ShouldBindJSON(&req); err != nil {
 		c.JSON(http.StatusBadRequest, Response{
 			Code:    400,
 			Message: err.Error(),
 		})
 		return
 	}

+	if req.Username == "" || req.Password == "" {
+		c.JSON(http.StatusBadRequest, Response{
+			Code:    400,
+			Message: "用户名和密码不能为空",
+		})
+		return
+	}
+
 	// 简单的模拟登录,任何用户名密码都可以登录
internal/server/log_handler.go (1)

104-113: 响应码不一致

GetMCPLogs 使用 Code: 0 表示成功,而 GetMCPLogStats 错误时使用 Code: 500。建议统一响应码规范:要么全部使用 0/非0,要么全部使用 HTTP 状态码。

internal/server/history_handler.go (1)

40-42: 软删除过滤可能冗余

如果 ChatLog 模型使用 gorm.DeletedAt 类型,GORM 会自动过滤软删除记录。当前模型使用 *time.Time,需要手动过滤。建议将模型中的 DeletedAt 改为 gorm.DeletedAt 类型以利用 GORM 的自动过滤功能。

相关修改在 internal/model/chat_log.go:

import "gorm.io/gorm"

type ChatLog struct {
    // ...
    DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
    // ...
}
internal/mcpclient/manager.go (1)

327-344: 建议将 RegisterFromDB 参数封装为结构体

方法签名过长(10个参数),降低了可读性和可维护性。建议使用结构体参数。

🔎 建议的修改
// DBServerConfig 从数据库注册 MCP 客户端所需的配置
type DBServerConfig struct {
    Name         string
    ServerType   string
    Command      string
    Args         []string
    Env          map[string]string
    BaseURL      string
    Headers      map[string]string
    Timeout      int
    ToolPrefix   string
    AutoRegister bool
}

// RegisterFromDB 从数据库模型注册 MCP 客户端
func (m *Manager) RegisterFromDB(cfg DBServerConfig) error {
    // 创建配置
    mcpCfg := &config.MCPServerConfig{
        Type:         cfg.ServerType,
        Command:      cfg.Command,
        // ...
    }
    return m.Register(cfg.Name, mcpCfg)
}
internal/llm/openai.go (1)

389-390: 考虑将最大工具调用次数设为可配置

maxIterations := 10 是硬编码值。对于复杂任务可能需要更多迭代,建议通过配置或参数传入。

🔎 建议的修改方向

可以在 Config 结构中添加 MaxToolIterations 字段,或者作为方法参数传入:

maxIterations := c.config.MaxToolIterations
if maxIterations <= 0 {
    maxIterations = 10 // 默认值
}
docs/INTEGRATION_EXAMPLE.go.example (2)

138-142: 优雅关闭的上下文管理问题

defer中的shutdown使用了5秒超时的新context,但外层的ctx在信号触发时会被cancel。建议使用独立的background context以确保shutdown有足够时间完成。

🔎 建议改进
 		// 优雅关闭
 		defer func() {
-			shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+			shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
-			defer cancel()
+			defer shutdownCancel()
 			httpServer.Stop(shutdownCtx)
 		}()

154-162: 示例代码的完整性

钉钉和飞书服务启动部分只有TODO注释,缺少具体的实现指导。建议添加伪代码或引用实际的实现位置,以帮助开发者理解如何集成这些服务。

docs/VERIFICATION_CHECKLIST.md (1)

28-179: API验证示例质量良好

提供了详细的curl命令示例,覆盖了所有主要配置端点(LLM、云厂商、IM、CICD、MCP、系统配置)。建议在实际验证时同时记录预期的响应格式示例,以便更好地验证API行为。

internal/database/migrate.go (1)

111-136: 数据库特定查询的兼容性

第113行使用了 SQLite 特定的 PRAGMA table_info 命令。如果将来考虑支持其他数据库(如PostgreSQL、MySQL),这段代码需要重构。

当前实现对于 SQLite 是正确的,但建议在文档中明确标注数据库依赖,或考虑使用GORM的抽象方法来检查列是否存在。

internal/feishu/handler.go (2)

34-65: 配置加载逻辑的降级策略良好

从数据库优先读取LLM配置,失败时降级到config.yaml的策略是合理的。日志信息也很清晰地标识了配置来源。

代码略显冗长,但为了清晰表达降级逻辑是可以接受的。


99-110: 消息持久化的错误处理

在第108行,保存用户消息失败时只记录错误日志而不中断流程。这个策略是合理的,因为持久化失败不应阻止用户获得响应。但需要确保有监控机制来跟踪持久化失败的频率。

建议添加持久化失败的指标监控,以便及时发现数据库问题。

internal/server/dingtalk.go (1)

261-275: 数据库保存失败时应考虑是否继续处理

当前实现中,用户消息保存失败仅记录日志但继续处理,这是合理的降级策略。但如果 userLognil(保存失败时),后续关联 AI 消息会丢失父级关联。

建议确认这是预期行为,或在日志中增加更明确的提示。

internal/server/version_handler.go (1)

46-56: 函数命名与方法重名可能造成混淆

包级函数 GetVersionInfoVersionHandler 的方法 GetVersionInfo 同名。虽然 Go 允许这样做,但可能影响代码可读性。

可以考虑将包级函数重命名为 HandleVersionInfo 或保持现状(如果团队规范允许)。

internal/server/dashboard_handler.go (3)

27-45: 数据库查询缺少错误处理

多个 Count 查询没有检查返回的错误。虽然在仪表盘场景下可以接受部分失败,但建议至少记录错误日志以便排查问题。

🔎 建议修复示例
 	// 统计活跃 IM 机器人数量
 	var activeBots int64
-	db.Model(&model.IMConfig{}).Where("enabled = ?", true).Count(&activeBots)
+	if err := db.Model(&model.IMConfig{}).Where("enabled = ?", true).Count(&activeBots).Error; err != nil {
+		// 记录错误但继续处理
+		logx.Error("Failed to count active bots: %v", err)
+	}

87-101: Uptime 使用硬编码占位值

TODO 注释表明 uptime 应从监控数据计算。当前硬编码值可能误导用户。

建议:

  1. 移除硬编码的百分比,显示 "N/A" 或 "-"
  2. 或者在 API 文档中说明这是占位值

135-140: 数据库状态检查过于简单

当前实现假设 SQLite 数据库始终在线。建议添加实际的健康检查,例如执行一个简单查询确认数据库连接正常。

🔎 建议修复
 	// 添加数据库状态
+	dbStatus := "online"
+	if err := db.Exec("SELECT 1").Error; err != nil {
+		dbStatus = "offline"
+	}
 	components = append(components, gin.H{
 		"label":  "SQLite Database",
-		"status": "online",
+		"status": dbStatus,
 		"uptime": "100%",
 	})
internal/wecom/handler.go (1)

206-214: 冗余变量声明

parentID 变量声明可以简化,直接使用 userLog.ID

🔎 建议修复
 	// 保存AI响应到数据库
 	if userLog != nil && aiResponse.Len() > 0 {
-		var parentID uint
-		parentID = userLog.ID
-		_, err := h.chatLogService.CreateAIMessage(username, source, aiResponse.String(), parentID)
+		_, err := h.chatLogService.CreateAIMessage(username, source, aiResponse.String(), userLog.ID)
 		if err != nil {
 			logx.Error("Failed to save AI response to database: %v", err)
 		}
 	}
internal/server/auth_handler.go (1)

62-68: HTTP 状态码使用不一致

认证失败返回 http.StatusOK (200) 但 body 中包含 Code: 401。这是一种 API 设计选择,但需要确保整个项目保持一致。

部分端点(如第 138-142 行)使用 http.StatusUnauthorized (401),建议统一风格。

Also applies to: 72-77, 81-86

internal/server/http.go (2)

88-92: 异步同步缺少错误恢复机制

此 goroutine 在启动时同步 IM 服务,但如果失败仅记录日志。考虑添加重试机制或在启动时进行阻塞式同步以确保服务状态一致性。


434-439: Response 结构体重复定义

Response 结构体在此处定义,但根据上下文 internal/model/common.go 中也有相同定义。建议统一使用 model.Response 以避免重复。

internal/server/service_handler.go (1)

36-49: 平台验证逻辑重复

validPlatforms map 在此处定义,但 GetPlatformStatus (第91-98行) 通过检查 status map 间接验证平台。建议将平台列表提取为常量以保持一致性。

🔎 建议的改进
+// validPlatforms 有效的平台列表
+var validPlatforms = map[string]bool{
+	"dingtalk": true,
+	"feishu":   true,
+	"wecom":    true,
+}
+
 // ToggleIMService 切换 IM 服务状态
 func (h *ServiceHandler) ToggleIMService(c *gin.Context) {
 	platform := c.Param("platform")
 	// ...
-	validPlatforms := map[string]bool{
-		"dingtalk": true,
-		"feishu":   true,
-		"wecom":    true,
-	}
-
 	if !validPlatforms[platform] {
internal/server/chat_handler.go (1)

419-429: 每次请求创建新的 ConfigService

GetModels 中每次请求都创建新的 ConfigService 实例。考虑在 ChatHandler 初始化时注入以提高效率。

🔎 建议的改进
 type ChatHandler struct {
 	config              *config.Config
 	chatLogService      *service.ChatLogService
 	conversationService *service.ConversationService
+	configService       *service.ConfigService
 	llmClient           *llm.Client
 	mcpServer           *imcp.MCPServer
 }
internal/service/conversation_service.go (1)

94-102: 硬编码的默认标题字符串

"新会话" 硬编码在此处,建议提取为常量以便维护和国际化。

🔎 建议的改进
+const DefaultConversationTitle = "新会话"
+
 // ShouldGenerateTitle 检查是否需要生成标题(标题为默认值且有消息)
 func (s *ConversationService) ShouldGenerateTitle(conversationID uint) (bool, error) {
 	conversation, err := s.GetConversation(conversationID)
 	if err != nil || conversation == nil {
 		return false, err
 	}
 	// 如果标题是"新会话",说明需要生成标题
-	return conversation.Title == "新会话", nil
+	return conversation.Title == DefaultConversationTitle, nil
 }
internal/service/chat_log_service.go (2)

120-143: 递归获取父消息可能导致 N+1 查询问题

GetConversationContext 通过循环调用 GetChatLogByID 获取父消息链,对于长对话链会产生大量数据库查询。

🔎 建议考虑使用单次查询优化

可以考虑使用递归 CTE(如果数据库支持)或预加载策略来减少查询次数:

// 示例:使用单次查询获取整个会话上下文
func (s *ChatLogService) GetConversationContext(messageID uint) ([]model.ChatLog, error) {
    // 如果会话ID存在,直接按会话ID查询所有消息
    var currentMsg model.ChatLog
    if err := s.db.First(&currentMsg, messageID).Error; err != nil {
        return nil, err
    }
    if currentMsg.ConversationID > 0 {
        var messages []model.ChatLog
        err := s.db.Where("conversation_id = ?", currentMsg.ConversationID).
            Order("created_at ASC").Find(&messages).Error
        return messages, err
    }
    // 降级到原有逻辑...
}

42-43: ChatType 使用魔术数字

ChatType 使用 1 和 2 表示用户提问和 AI 回答,建议定义常量以提高代码可读性。

🔎 建议的改进
+const (
+	ChatTypeUser = 1 // 用户提问
+	ChatTypeAI   = 2 // AI回答
+)
+
 // CreateUserMessageWithConversation 创建用户消息日志(带会话ID)
 func (s *ChatLogService) CreateUserMessageWithConversation(username, source, content string, conversationID uint) (*model.ChatLog, error) {
 	log := &model.ChatLog{
 		Username:       username,
 		Source:         source,
-		ChatType:       1, // 1=用户提问
+		ChatType:       ChatTypeUser,
 		ParentContent:  0,

Also applies to: 64-65

internal/server/conversation_handler.go (1)

11-15: chatLogService 字段未被使用

chatLogService 被注入但在此处理器中未使用任何方法。消息获取通过 conversationService.GetConversationMessages 完成。可以移除此字段或确认是否有计划使用。

🔎 建议的改进
 // ConversationHandler 会话处理器
 type ConversationHandler struct {
 	conversationService *service.ConversationService
-	chatLogService      *service.ChatLogService
 }
 
 // NewConversationHandler 创建会话处理器
 func NewConversationHandler() *ConversationHandler {
 	return &ConversationHandler{
 		conversationService: service.NewConversationService(),
-		chatLogService:      service.NewChatLogService(),
 	}
 }
internal/server/service_manager.go (3)

78-82: 字段映射容易引起混淆,AppKey 被赋值给 AppID

第 80-81 行的字段映射看起来不一致:imConfig.AppID 被赋值给 sm.config.DingTalk.AppKey,而 imConfig.AppKey 被赋值给 AppSecret。这种交叉映射容易导致维护困难和潜在的配置错误。

建议添加注释说明映射关系,或在数据模型层统一字段命名。


362-363: 使用 time.Sleep 进行同步是不可靠的做法

time.Sleep(100 * time.Millisecond) 用于等待服务初始化是一种代码异味。这个延迟可能在某些环境下不够,而在其他环境下又太长。

建议使用更可靠的同步机制,如等待服务就绪信号或轮询检查服务状态。

🔎 建议的修复方案
 // UpdateAndToggle 更新配置并切换服务状态
 func (sm *ServiceManager) UpdateAndToggle(ctx context.Context, imConfig *model.IMConfig) error {
 	// 先保存配置到数据库
 	if err := sm.configService.SaveIMConfig(imConfig); err != nil {
 		return fmt.Errorf("failed to save config: %w", err)
 	}
 
-	// 给服务一点时间初始化
-	time.Sleep(100 * time.Millisecond)
+	// TODO: 考虑使用更可靠的同步机制,例如等待配置生效的回调
 
 	// 切换服务状态
 	return sm.ToggleService(ctx, imConfig.Platform, imConfig.Enabled)
 }

243-251: Wecom 配置的字段映射同样令人困惑

第 250-251 行:imConfig.AppID 赋值给 TokenimConfig.AppKey 赋值给 EncodingAESKey。这与其他 IM 平台的映射方式不一致,且语义上不明确。

internal/service/config_migration.go (2)

12-43: 迁移过程缺乏事务保护,部分失败可能导致数据不一致

MigrateFromYAML 函数依次迁移多个配置域,但没有使用数据库事务。如果在迁移过程中某一步失败(例如第 3 步 IM 配置迁移失败),之前已成功的迁移(LLM、Providers)不会回滚,可能导致数据处于不一致状态。

🔎 建议使用事务包装迁移过程
 func (s *ConfigService) MigrateFromYAML(cfg *config.Config) error {
 	log.Println("Starting configuration migration from YAML to database...")
 
+	return s.db.Transaction(func(tx *gorm.DB) error {
+		// 创建临时服务使用事务
+		txService := &ConfigService{db: tx}
+
-	// 1. 迁移 LLM 配置
-	if err := s.migrateLLMConfig(cfg.LLM); err != nil {
+	if err := txService.migrateLLMConfig(cfg.LLM); err != nil {
 		return fmt.Errorf("failed to migrate LLM config: %w", err)
 	}
+		// ... 其他迁移步骤也使用 txService
+		return nil
+	})
 }

508-516: getSystemConfigInt 使用 fmt.Sscanf 解析整数但未处理解析失败的情况

当配置值不是有效整数时,Sscanf 会返回错误,但调用方(如第 452 行)只检查 err == nil,这是正确的。不过,建议使用 strconv.Atoi 以获得更清晰的语义。

internal/server/config_handler.go (3)

1107-1111: 用户名硬编码为 "admin",应从会话中获取

第 1110 行的注释已经说明这是一个待改进的点。在生产环境中,应该从用户会话或认证上下文中获取实际用户名,以便正确记录审计日志。

如果需要,我可以帮助实现从 Gin 上下文中获取认证用户的逻辑。


1033-1046: 存在 TODO 注释标记的未完成功能

GetMCPTools 方法目前返回空列表,注释表明需要与 MCP 服务器通信来获取实际工具列表。

是否需要我帮助实现此功能,或者创建一个 issue 来跟踪此任务?


82-112: Header 格式迁移逻辑存在重复代码

InitializeMCPServersFromDB 中的 header 解析和迁移逻辑(第 82-112 行)与 ToggleMCPServer 中的逻辑(第 890-924 行)高度相似。建议提取为公共辅助函数。

🔎 建议提取公共函数
// normalizeServerHeaders 规范化服务器 Headers 格式并可选更新数据库
func normalizeServerHeaders(server *model.MCPServer, configService *service.ConfigService) map[string]string {
    headers := make(map[string]string)
    if server.Headers == nil {
        return headers
    }
    
    if customHeader, ok := server.Headers["custom"]; ok {
        if customHeaderStr, isString := customHeader.(string); isString {
            headers = parseHeaderString(customHeaderStr)
            // 更新数据库中的格式
            server.Headers = make(map[string]interface{})
            for k, v := range headers {
                server.Headers[k] = v
            }
            if configService != nil {
                configService.UpdateMCPServer(server)
            }
        }
    } else {
        for k, v := range server.Headers {
            if strVal, ok := v.(string); ok {
                headers[k] = strVal
            }
        }
    }
    return headers
}
internal/service/config_service.go (2)

296-322: UpsertMCPTool 使用 SQLite 特定语法,可能影响数据库迁移

第 311-320 行使用 ON CONFLICT ... DO UPDATE 语法,这是 SQLite 特定的。如果未来需要支持 MySQL 或 PostgreSQL,此代码需要修改。

建议添加注释说明此限制,或使用 GORM 的 Clauses(clause.OnConflict{...}) 实现跨数据库兼容。

🔎 使用 GORM Clauses 实现跨数据库兼容
+import "gorm.io/gorm/clause"
+
 func (s *ConfigService) UpsertMCPTool(tool *model.MCPTool) error {
-	// 使用原生SQL实现真正的Upsert
-	// SQLite使用 INSERT ... ON CONFLICT ... DO UPDATE
-	inputSchemaJSON, err := json.Marshal(tool.InputSchema)
-	if err != nil {
-		return err
-	}
-
 	now := time.Now()
 	if tool.CreatedAt.IsZero() {
 		tool.CreatedAt = now
 	}
 	tool.UpdatedAt = now
 
-	return s.db.Exec(`...`).Error
+	return s.db.Clauses(clause.OnConflict{
+		Columns:   []clause.Column{{Name: "server_id"}, {Name: "name"}},
+		DoUpdates: clause.AssignmentColumns([]string{"description", "is_enabled", "input_schema", "updated_at"}),
+	}).Create(tool).Error
 }

26-29: 暴露底层数据库连接可能破坏封装性

GetDB() 方法直接返回 *gorm.DB,允许调用方绕过 ConfigService 直接操作数据库。这可能导致数据一致性问题和难以追踪的数据库操作。

如果确实需要此功能(例如用于事务),建议改为提供 WithTransaction 方法。

Comment on lines +27 to +35
- name: 设置 Node.js 环境
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: 'zenops-web/package-lock.json'

- name: 构建前端
run: make build-web
Copy link

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

🏁 Script executed:

# Check Makefile for build-web target
echo "=== Checking Makefile build-web target ==="
rg -A 5 "^build-web:" Makefile

Repository: opsre/ZenOps

Length of output: 259


🏁 Script executed:

# Check for documentation file
echo "=== Checking for FINAL_IMPLEMENTATION_SUMMARY.md ==="
fd "FINAL_IMPLEMENTATION_SUMMARY.md" 

# If found, check line 214 for pnpm mention
if [ -f "FINAL_IMPLEMENTATION_SUMMARY.md" ]; then
  echo "=== Checking line 214 in FINAL_IMPLEMENTATION_SUMMARY.md ==="
  sed -n '210,220p' FINAL_IMPLEMENTATION_SUMMARY.md
fi

Repository: opsre/ZenOps

Length of output: 145


🏁 Script executed:

# Check what lockfiles exist in zenops-web directory
echo "=== Checking lockfiles in zenops-web ==="
ls -la zenops-web/ | grep -E "(package-lock|pnpm-lock|yarn.lock)"

Repository: opsre/ZenOps

Length of output: 96


🏁 Script executed:

# Check the zenops-web directory structure
echo "=== Checking zenops-web directory structure ==="
ls -la zenops-web/ | head -20

Repository: opsre/ZenOps

Length of output: 214


🏁 Script executed:

# Check package.json in zenops-web for package manager hint
echo "=== Checking zenops-web/package.json ==="
cat zenops-web/package.json 2>/dev/null | head -30

Repository: opsre/ZenOps

Length of output: 96


🏁 Script executed:

# Check the content around line 214 in FINAL_IMPLEMENTATION_SUMMARY.md
echo "=== Checking FINAL_IMPLEMENTATION_SUMMARY.md for pnpm mention ==="
sed -n '200,230p' docs/FINAL_IMPLEMENTATION_SUMMARY.md

echo -e "\n=== Searching for pnpm mentions throughout the file ==="
rg "pnpm" docs/FINAL_IMPLEMENTATION_SUMMARY.md

Repository: opsre/ZenOps

Length of output: 730


🏁 Script executed:

# Check if zenops-web directory exists and its actual contents
echo "=== Verify zenops-web directory ==="
test -d zenops-web && echo "Directory exists" || echo "Directory does NOT exist"

# List all directories in repository root
echo -e "\n=== Root level directories ==="
ls -la | grep "^d"

Repository: opsre/ZenOps

Length of output: 761


🏁 Script executed:

# Search for references to package managers in the repository
echo "=== Searching for package manager references ==="
rg "pnpm|npm" docs/ --type md

Repository: opsre/ZenOps

Length of output: 744


🏁 Script executed:

# Check the web directory (frontend) contents
echo "=== Checking web directory ==="
ls -la web/

echo -e "\n=== Checking for lockfiles in web directory ==="
ls -la web/ | grep -E "(package-lock|pnpm-lock|yarn.lock)"

# Check if web directory has package.json
echo -e "\n=== Checking web/package.json ==="
head -30 web/package.json 2>/dev/null || echo "No package.json found"

Repository: opsre/ZenOps

Length of output: 361


🏁 Script executed:

# Check the actual WEB_DIR variable in Makefile
echo "=== Checking WEB_DIR variable in Makefile ==="
rg "WEB_DIR" Makefile

# Check if there's a pnpm-lock.yaml anywhere
echo -e "\n=== Searching for pnpm-lock.yaml ==="
fd "pnpm-lock.yaml"

# Check if there's a package-lock.json anywhere
echo -e "\n=== Searching for package-lock.json ==="
fd "package-lock.json"

Repository: opsre/ZenOps

Length of output: 278


🏁 Script executed:

# Check the release.yml workflow to see the exact cache configuration
echo "=== Checking release.yml cache configuration ==="
sed -n '25,40p' .github/workflows/release.yml

# Check if there's any frontend-related code elsewhere
echo -e "\n=== Searching for package.json files ==="
fd "package.json"

# Check the git status of zenops-web
echo -e "\n=== Checking git status of zenops-web ==="
git ls-files zenops-web/ 2>/dev/null || echo "No files tracked in zenops-web"

Repository: opsre/ZenOps

Length of output: 594


修复工作流配置:前端缓存路径不存在且包管理器配置不一致

工作流在 line 31-32 指定 cache: 'npm'cache-dependency-path: 'zenops-web/package-lock.json',但存在以下问题:

  1. zenops-web/ 目录为空,不存在 package-lock.json 文件,将导致缓存步骤失败
  2. 整个仓库中不存在任何 package.json 文件,前端源代码缺失
  3. 文档 FINAL_IMPLEMENTATION_SUMMARY.md 中使用 pnpm 作为包管理器,但 Makefile 中 build-web 目标使用 npm install && npm run build

需要确认前端代码的实际位置,检查是否:

  • 前端源代码应该在其他分支/目录中
  • 缓存配置应该指向正确的 lockfile 位置
  • Makefile 和工作流需要使用一致的包管理器

Comment on lines +6 to +21
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"

"github.com/eryajf/zenops/internal/config"
"github.com/eryajf/zenops/internal/database"
"github.com/eryajf/zenops/internal/imcp"
"github.com/eryajf/zenops/internal/mcpclient"
"github.com/eryajf/zenops/internal/server"
"github.com/eryajf/zenops/internal/service"
"github.com/spf13/cobra"
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

缺少必要的包导入

在第139行使用了 time.Second,但导入列表中缺少 "time" 包。这会导致编译错误。

🔎 建议修复
 import (
 	"context"
 	"fmt"
 	"log"
 	"os"
 	"os/signal"
 	"syscall"
+	"time"
 
 	"github.com/eryajf/zenops/internal/config"
📝 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
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"github.com/eryajf/zenops/internal/config"
"github.com/eryajf/zenops/internal/database"
"github.com/eryajf/zenops/internal/imcp"
"github.com/eryajf/zenops/internal/mcpclient"
"github.com/eryajf/zenops/internal/server"
"github.com/eryajf/zenops/internal/service"
"github.com/spf13/cobra"
)
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/eryajf/zenops/internal/config"
"github.com/eryajf/zenops/internal/database"
"github.com/eryajf/zenops/internal/imcp"
"github.com/eryajf/zenops/internal/mcpclient"
"github.com/eryajf/zenops/internal/server"
"github.com/eryajf/zenops/internal/service"
"github.com/spf13/cobra"
)
🤖 Prompt for AI Agents
In docs/INTEGRATION_EXAMPLE.go.example around lines 6 to 21, the code uses
time.Second later but the "time" package is not imported; add "time" to the
import block (e.g., include "time" alongside the other imports) so the reference
to time.Second compiles correctly.

Comment on lines +12 to +13
// JWT 密钥,实际生产环境应该从配置文件中读取
var JWTSecret = []byte("zenops-jwt-secret-key-change-in-production")
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

安全问题:JWT 密钥硬编码

JWT 密钥直接硬编码在源代码中是严重的安全隐患。即使注释说明应从配置文件读取,但当前代码在生产环境仍会使用此硬编码值。

攻击者可以使用此密钥伪造任意用户的 token。

🔎 建议修复
-// JWT 密钥,实际生产环境应该从配置文件中读取
-var JWTSecret = []byte("zenops-jwt-secret-key-change-in-production")
+// JWTSecret JWT 密钥,必须从配置文件或环境变量中读取
+var JWTSecret []byte
+
+// InitJWTSecret 初始化 JWT 密钥
+func InitJWTSecret(secret string) {
+	if secret == "" {
+		panic("JWT secret must not be empty")
+	}
+	JWTSecret = []byte(secret)
+}

然后在应用启动时从配置中初始化:

// 在 main 或 init 中调用
middleware.InitJWTSecret(cfg.Auth.JWTSecret)

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

🤖 Prompt for AI Agents
In internal/middleware/auth.go around lines 12-13 the JWTSecret is hardcoded
which is insecure; remove the hardcoded value and instead expose a setter (e.g.,
InitJWTSecret) or initialize JWTSecret from configuration/environment at
startup. Update the package to accept the secret from your app config (or
os.Env) during main/init, validate it is non-empty and return an error or exit
startup if missing, and ensure no fallback to the hardcoded literal remains in
the codebase.

@eryajf eryajf merged commit 9ba045c into main Jan 1, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants