diff --git a/cmd/root.go b/cmd/root.go index 51b82c1..47d7418 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -10,7 +10,9 @@ import ( "time" "cnb.cool/zhiqiangwang/pkg/logx" + "github.com/eryajf/zenops/internal/agent" "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/provider/aliyun" // 注册 aliyun provider @@ -152,7 +154,18 @@ var runCmd = &cobra.Command{ // 4. 创建 MCP 服务器 (钉钉和飞书共享) mcpServer := imcp.NewMCPServer(cfg) - // 5. 注册外部 MCP 的工具 (如果启用) + // 5. 初始化 Agent 系统 (Memory Manager, Knowledge Retriever, Orchestrator, Stream Handler) + db := database.GetDB() + agentSystem, err := agent.Initialize(ctx, db, mcpServer, cfg) + if err != nil { + logx.Error("❌ Failed to initialize Agent system: %v", err) + } else { + logx.Info("✅ Agent system ready for use") + // 设置全局 Agent (供 HTTP 服务使用) + server.SetGlobalAgent(agentSystem) + } + + // 6. 注册外部 MCP 的工具 (如果启用) if cfg.Server.MCP.AutoRegisterExternalTools { logx.Info("🔧 Registering external MCP tools...") if err := mcpServer.RegisterExternalMCPTools(ctx, mcpClientManager); err != nil { diff --git a/config.example.yaml b/config.example.yaml index 17cf358..bd3081b 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -100,3 +100,10 @@ cache: enabled: true type: "memory" # memory 或 redis ttl: 300 # 缓存过期时间(秒) + # Redis 配置 (当 type 为 redis 时生效) + redis: + host: "localhost" + port: 6379 + password: "" # Redis 密码,无密码留空 + db: 0 # Redis 数据库编号 (0-15) + pool_size: 10 # 连接池大小 diff --git a/docs/plans/2026-01-02-eino-integration-design.md b/docs/plans/2026-01-02-eino-integration-design.md new file mode 100644 index 0000000..5a81a61 --- /dev/null +++ b/docs/plans/2026-01-02-eino-integration-design.md @@ -0,0 +1,1241 @@ +# ZenOps Eino 框架集成设计方案 + +**文档版本**: v1.0 +**创建日期**: 2026-01-02 +**作者**: Claude +**状态**: 待审核 + +--- + +## 1. 背景和目标 + +### 1.1 当前问题 + +ZenOps 现有的 LLM 对话能力基于简单的请求-响应模式,存在以下局限性: + +1. **处理复杂问题能力不足** + - 需要多轮 MCP 调用才能获取足够信息 + - 无法跨多个 MCP Server 进行智能编排 + - 缺乏自动推理和规划能力 + +2. **缺乏上下文记忆** + - 无法利用用户的历史对话信息 + - 每次查询都是独立的,无法进行追问式交互 + - 高频问题重复处理,效率低下 + +3. **知识库能力缺失** + - 无法让用户配置常用资料信息 + - 不支持文档解析和知识检索 + - 回答准确性依赖 LLM 本身知识 + +### 1.2 改造目标 + +引入 **Eino 框架**,实现以下能力提升: + +✅ **智能编排**: 支持多步骤、跨 MCP Server 的复杂任务自动推理和执行 +✅ **记忆管理**: 基于 SQLite + Redis 的会话记忆和用户上下文管理 +✅ **知识增强**: 支持文档解析、向量检索和知识库配置 +✅ **流式优化**: 保持现有钉钉/飞书/企微流式输出能力 +✅ **代码简化**: 用 Eino 统一抽象替换分散的 LLM 调用逻辑 + +--- + +## 2. Eino 框架调研 + +### 2.1 框架概述 + +[Eino](https://github.com/cloudwego/eino) 是字节跳动开源的 Go 语言 LLM 应用开发框架,已在抖音、豆包等产品中经过生产验证。 + +**核心特性**: +- 强类型、符合 Go 语言习惯的 API 设计 +- 丰富的组件抽象(ChatModel、Tool、Retriever、Lambda 等) +- 强大的编排能力(Chain、Graph、Workflow) +- 内置 ReAct Agent 实现 +- 原生支持 MCP 协议集成 + +### 2.2 核心组件 + +| 组件 | 说明 | 在 ZenOps 中的应用 | +|------|------|-------------------| +| **ChatModel** | LLM 接口抽象 | 替换现有 `internal/llm/openai.go` | +| **Tool** | 工具调用接口 | 将 MCP Server 适配为 Eino Tool | +| **Retriever** | 文档检索接口 | 实现知识库检索 | +| **Graph** | 有向图编排 | 实现复杂的多步骤任务流程 | +| **ChatMemory** | 会话记忆 | 基于 SQLite + Redis 实现 | + +### 2.3 MCP 集成 + +Eino 通过适配器模式支持 MCP 协议: +- 使用 `github.com/mark3labs/mcp-go` SDK(与 ZenOps 现有依赖一致) +- 支持 stdio、SSE、streamableHttp 三种传输协议 +- 可将 MCP Server 的工具直接包装为 Eino Tool + +**参考资料**: +- [Eino MCP Tool 集成文档](https://cloudwego.cn/docs/eino/ecosystem_integration/tool/tool_mcp/) +- [MCP Go SDK](https://github.com/mark3labs/mcp-go) + +--- + +## 3. 整体架构设计 + +### 3.1 架构图 + +``` +┌─────────────────────────────────────────────────────────┐ +│ 用户请求入口 │ +│ (钉钉/飞书/企微/Web/CLI/HTTP API) │ +└─────────────────┬───────────────────────────────────────┘ + │ +┌─────────────────▼───────────────────────────────────────┐ +│ Eino Agent Orchestrator │ +│ (Graph 编排 + ReAct 推理引擎) │ +├──────────────────────────────────────────────────────────┤ +│ ┌────────────┐ ┌──────────────┐ ┌─────────────────┐ │ +│ │ Memory │ │ Knowledge │ │ MCP Tools │ │ +│ │ Manager │ │ Retriever │ │ Adapter │ │ +│ └─────┬──────┘ └──────┬───────┘ └────────┬────────┘ │ +│ │ │ │ │ +│ ┌─────▼────────────────▼───────────────────▼─────────┐ │ +│ │ Eino ChatModel (OpenAI 兼容) │ │ +│ └──────────────────────────────────────────────────────┘ │ +└──────────────────┬──────────────────┬───────────────────┘ + │ │ + ┌───────────▼────────┐ ┌────▼──────────────┐ + │ Redis (L1 Cache) │ │ SQLite (L2 DB) │ + │ - 会话状态 │ │ - 用户上下文 │ + │ - QA 缓存 │ │ - 对话历史 │ + │ - 活跃会话 │ │ - 知识库 │ + └────────────────────┘ └───────────────────┘ + │ + ┌───────────▼────────────────────────────────┐ + │ MCP Client Manager │ + │ (复用现有 internal/mcpclient) │ + │ - stdio/SSE/streamableHttp │ + └────────────────────────────────────────────┘ +``` + +### 3.2 数据流程 + +**简单问答流程**: +``` +用户提问 + → 检查 QA 缓存 (Redis) + ├─ 命中 → 直接返回 + └─ 未命中 ↓ + → 加载对话历史 (Redis/SQLite) + → 加载用户上下文 (SQLite) + → 检索知识库 (SQLite FTS5) + → Eino Graph 编排 + → ChatModel 推理 + → 判断是否需要工具调用 + ├─ 不需要 → 直接回答 + └─ 需要 → 调用 MCP Tools + → 返回结果给 ChatModel + → (可能多轮循环) + → 保存到记忆 (Redis + SQLite) + → 更新 QA 缓存 + → 返回用户 +``` + +**复杂任务流程示例**(跨 MCP Server): +``` +用户: "对比阿里云和腾讯云的 CVM 数量,生成报告" + +Eino Graph 自动编排: + 1. 调用 MCP Tool: aliyun_list_ecs + 2. 调用 MCP Tool: tencent_list_cvm + 3. LLM 汇总分析两者数据 + 4. 生成对比报告 + 5. 返回给用户 +``` + +### 3.3 存储策略 + +#### SQLite (持久化存储) +- **现有表**: `chat_logs`, `conversations`, `users` 等(保留) +- **新增表**: `user_contexts`, `qa_cache`, `knowledge_documents`, `knowledge_fts` + +#### Redis (缓存层) +- **Key 设计**: + - `conv:{conversation_id}:history` → 对话历史 (List, TTL=1h) + - `user:{username}:context` → 用户上下文 (Hash) + - `qa:{question_hash}` → 问答缓存 (String, TTL=1h) + - `session:{username}:active` → 当前活跃会话 ID (String) + +--- + +## 4. 详细模块设计 + +### 4.1 Memory Manager(记忆管理) + +**职责**: 管理会话历史、用户上下文和 QA 缓存 + +**接口定义**: +```go +// internal/memory/manager.go + +type MemoryManager struct { + redis *redis.Client + db *gorm.DB + ttl time.Duration +} + +// 核心方法 +func (m *MemoryManager) GetConversationHistory(conversationID uint, limit int) ([]*model.ChatLog, error) +func (m *MemoryManager) SaveMessage(conversationID uint, chatType int, content string) error +func (m *MemoryManager) GetUserContext(username string) (*UserContext, error) +func (m *MemoryManager) UpdateUserContext(username, key, value string) error +func (m *MemoryManager) GetCachedAnswer(username, question string) (string, bool, error) +func (m *MemoryManager) UpdateQACache(username, question, answer string) error +``` + +**新增数据库表**: + +```sql +-- 用户上下文表(扩展用户偏好) +CREATE TABLE user_contexts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_at DATETIME, + updated_at DATETIME, + username TEXT NOT NULL, + context_key TEXT NOT NULL, -- 如: "favorite_region", "default_vpc" + context_value TEXT, -- JSON 格式存储值 + context_type TEXT DEFAULT 'user', -- user/system/auto_learned + UNIQUE(username, context_key) +); +CREATE INDEX idx_user_contexts_username ON user_contexts(username); + +-- 问答缓存表(语义缓存) +CREATE TABLE qa_cache ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_at DATETIME, + updated_at DATETIME, + question_hash TEXT NOT NULL, -- 问题的语义哈希 + question TEXT NOT NULL, + answer TEXT, + username TEXT, -- 可选:用户级别缓存 + hit_count INTEGER DEFAULT 1, + last_hit_at DATETIME, + UNIQUE(question_hash, username) +); +CREATE INDEX idx_qa_cache_hash ON qa_cache(question_hash); +CREATE INDEX idx_qa_cache_hits ON qa_cache(hit_count DESC); + +-- 为 chat_logs 添加全文索引(可选,用于历史搜索) +CREATE VIRTUAL TABLE chat_logs_fts USING fts5( + content, + content='chat_logs', + content_rowid='id' +); +``` + +**工作流程**: +1. **读取历史**: 先查 Redis `conv:{id}:history`,未命中则从 `chat_logs` 表加载并回填 +2. **用户上下文**: 从 `user_contexts` 表读取,注入到 System Prompt +3. **QA 缓存**: 对问题计算哈希,查询 `qa_cache` 表,命中则返回并更新 `hit_count` + +--- + +### 4.2 Knowledge Retriever(知识检索) + +**职责**: 文档解析、存储和智能检索 + +**接口定义**: +```go +// internal/knowledge/retriever.go + +type KnowledgeRetriever struct { + db *gorm.DB + embedder *Embedder // 文本向量化(可选) + useVector bool // 是否启用向量检索 +} + +// 实现 Eino Retriever 接口 +func (k *KnowledgeRetriever) Retrieve(ctx context.Context, query string, opts ...Option) ([]*Document, error) + +// 文档管理 +func (k *KnowledgeRetriever) AddDocument(doc *Document) error +func (k *KnowledgeRetriever) DeleteDocument(docID int) error +func (k *KnowledgeRetriever) ListDocuments(category string) ([]*Document, error) +``` + +**新增数据库表**: + +```sql +-- 知识库文档表 +CREATE TABLE knowledge_documents ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_at DATETIME, + updated_at DATETIME, + doc_type TEXT, -- 'markdown', 'pdf', 'url', 'manual' + title TEXT, + content TEXT, + metadata JSON, -- 存储来源、作者等元信息 + enabled INTEGER DEFAULT 1, + category TEXT -- 分类:运维文档、API文档等 +); +CREATE INDEX idx_knowledge_category ON knowledge_documents(category, enabled); + +-- 文档全文索引(SQLite FTS5) +CREATE VIRTUAL TABLE knowledge_fts USING fts5( + title, + content, + content='knowledge_documents', + content_rowid='id', + tokenize='porter unicode61' -- 支持中英文分词 +); + +-- 可选:向量表(如果启用 sqlite-vec) +-- 需要 sqlite-vec 扩展支持 +CREATE VIRTUAL TABLE IF NOT EXISTS knowledge_vectors USING vec0( + doc_id INTEGER PRIMARY KEY, + embedding FLOAT[1536] -- OpenAI embedding 维度 +); +``` + +**检索策略**: +1. **关键词检索**(FTS5): 快速全文搜索,适合精确匹配 +2. **向量检索**(可选): 语义相似度搜索,适合模糊查询 +3. **混合检索**: RRF (Reciprocal Rank Fusion) 算法合并结果 + +**文档导入方式**: +- 管理员在 Web 界面上传/配置文档 +- 从高频 QA 缓存中自动提取知识(`hit_count > 阈值`) +- 定期抓取外部运维文档(Confluence、Wiki 等) + +--- + +### 4.3 Agent Orchestrator(智能编排) + +**职责**: 核心编排引擎,使用 Eino Graph 管理复杂对话流程 + +**接口定义**: +```go +// internal/agent/orchestrator.go + +type AgentOrchestrator struct { + chatModel chatmodel.ChatModel // Eino ChatModel + memoryMgr *memory.MemoryManager + knowledgeRet *knowledge.KnowledgeRetriever + mcpServer *imcp.Server // 复用现有 MCP Server + toolsNode *compose.ToolsNode +} + +// 构建 Eino Graph +func (a *AgentOrchestrator) BuildGraph() *compose.Graph + +// 执行对话 +func (a *AgentOrchestrator) Execute(ctx context.Context, req *ChatRequest) (*ChatResponse, error) + +// 流式对话 +func (a *AgentOrchestrator) Stream(ctx context.Context, req *ChatRequest) (<-chan string, error) +``` + +**Graph 定义**: + +```go +func (a *AgentOrchestrator) BuildGraph() *compose.Graph { + builder := compose.NewGraphBuilder[map[string]any]() + + // 节点定义 + builder.AddNode("load_memory", a.loadMemoryNode) // 加载历史 + builder.AddNode("load_context", a.loadContextNode) // 加载用户上下文 + builder.AddNode("retrieve_knowledge", a.retrieveKnowledgeNode) // 检索知识库 + builder.AddNode("llm", a.llmNode) // LLM 推理 + builder.AddNode("tools", a.toolsNode) // MCP 工具调用 + builder.AddNode("save_memory", a.saveMemoryNode) // 保存历史 + + // 边定义(流程编排) + builder.AddEdge(START, "load_memory") + builder.AddEdge("load_memory", "load_context") + builder.AddEdge("load_context", "retrieve_knowledge") + builder.AddEdge("retrieve_knowledge", "llm") + + // 条件分支:是否需要调用工具 + builder.AddConditionalEdge("llm", a.shouldCallTools, + map[string]string{ + "tools": "tools", // 需要调用工具 + "finish": "save_memory", // 直接结束 + }) + + builder.AddEdge("tools", "llm") // 工具结果回到 LLM(支持多轮) + builder.AddEdge("save_memory", END) + + return builder.Compile() +} + +// 条件路由:判断是否需要调用工具 +func (a *AgentOrchestrator) shouldCallTools(state map[string]any) string { + response := state["llm_response"].(ChatResponse) + if len(response.ToolCalls) > 0 { + return "tools" + } + return "finish" +} +``` + +**关键节点实现**: + +```go +// 1. 加载记忆节点 +func (a *AgentOrchestrator) loadMemoryNode(ctx context.Context, state map[string]any) (map[string]any, error) { + conversationID := state["conversation_id"].(uint) + history, err := a.memoryMgr.GetConversationHistory(conversationID, 10) + if err != nil { + return state, err + } + state["history"] = history + return state, nil +} + +// 2. 加载用户上下文节点 +func (a *AgentOrchestrator) loadContextNode(ctx context.Context, state map[string]any) (map[string]any, error) { + username := state["username"].(string) + userCtx, err := a.memoryMgr.GetUserContext(username) + if err != nil { + return state, err + } + state["user_context"] = userCtx + return state, nil +} + +// 3. 检索知识库节点 +func (a *AgentOrchestrator) retrieveKnowledgeNode(ctx context.Context, state map[string]any) (map[string]any, error) { + userMessage := state["user_message"].(string) + docs, err := a.knowledgeRet.Retrieve(ctx, userMessage) + if err != nil { + return state, err + } + state["knowledge_docs"] = docs + return state, nil +} + +// 4. LLM 推理节点 +func (a *AgentOrchestrator) llmNode(ctx context.Context, state map[string]any) (map[string]any, error) { + // 构建完整的 Prompt(包含历史、上下文、知识库) + messages := a.buildMessages(state) + + // 调用 Eino ChatModel + resp, err := a.chatModel.Generate(ctx, messages, chatmodel.WithTools(a.getTools())) + if err != nil { + return state, err + } + + state["llm_response"] = resp + return state, nil +} + +// 5. 工具调用节点(使用 Eino ToolsNode) +func (a *AgentOrchestrator) buildToolsNode() *compose.ToolsNode { + return compose.NewToolsNode(a.buildMCPTools()) +} + +// 6. 保存记忆节点 +func (a *AgentOrchestrator) saveMemoryNode(ctx context.Context, state map[string]any) (map[string]any, error) { + conversationID := state["conversation_id"].(uint) + userMessage := state["user_message"].(string) + aiResponse := state["llm_response"].(ChatResponse) + + // 保存用户消息 + _ = a.memoryMgr.SaveMessage(conversationID, 1, userMessage) + + // 保存 AI 回复 + _ = a.memoryMgr.SaveMessage(conversationID, 2, aiResponse.Content) + + // 更新 QA 缓存 + username := state["username"].(string) + _ = a.memoryMgr.UpdateQACache(username, userMessage, aiResponse.Content) + + return state, nil +} +``` + +**MCP Tools 适配器**: + +```go +// internal/agent/mcp_adapter.go + +type MCPToolAdapter struct { + name string + desc string + schema any + mcpServer *imcp.Server +} + +// 实现 Eino Tool 接口 +func (t *MCPToolAdapter) Info(ctx context.Context) (*schema.ToolInfo, error) { + return &schema.ToolInfo{ + Name: t.name, + Description: t.desc, + ParamsOneOf: t.schema, + }, nil +} + +func (t *MCPToolAdapter) InvokableRun(ctx context.Context, args string, opts ...Option) (string, error) { + // 解析参数 + var params map[string]any + if err := json.Unmarshal([]byte(args), ¶ms); err != nil { + return "", err + } + + // 调用 MCP Server + result, err := t.mcpServer.CallTool(ctx, t.name, params) + if err != nil { + return "", err + } + + // 提取文本结果 + if len(result.Content) > 0 { + if textContent, ok := result.Content[0].(mcp.TextContent); ok { + return textContent.Text, nil + } + } + + return "", nil +} + +// 从 MCP Server 构建 Eino Tools +func (a *AgentOrchestrator) buildMCPTools() []tool.Tool { + var tools []tool.Tool + + mcpTools, _ := a.mcpServer.ListEnabledTools(context.Background()) + + for _, mcpTool := range mcpTools.Tools { + adapter := &MCPToolAdapter{ + name: mcpTool.Name, + desc: mcpTool.Description, + schema: mcpTool.InputSchema, + mcpServer: a.mcpServer, + } + tools = append(tools, adapter) + } + + return tools +} +``` + +--- + +### 4.4 Stream Handler(流式输出) + +**职责**: 适配 Eino 流式输出到现有 IM 接口 + +**接口定义**: +```go +// internal/agent/stream_handler.go + +type StreamHandler struct { + orchestrator *AgentOrchestrator +} + +// 流式对话(兼容现有接口) +func (s *StreamHandler) ChatStream(ctx context.Context, req *ChatRequest) (<-chan string, error) { + responseCh := make(chan string, 100) + + go func() { + defer close(responseCh) + + // 构建初始状态 + state := map[string]any{ + "user_message": req.Message, + "username": req.Username, + "conversation_id": req.ConversationID, + } + + // 执行 Eino Graph(带回调) + graph := s.orchestrator.BuildGraph() + callbacks := &StreamCallbacks{responseCh: responseCh} + + err := graph.Stream(ctx, state, compose.WithCallbacks(callbacks)) + if err != nil { + responseCh <- fmt.Sprintf("❌ 执行失败: %v", err) + } + }() + + return responseCh, nil +} +``` + +**流式回调**: +```go +type StreamCallbacks struct { + responseCh chan<- string +} + +func (c *StreamCallbacks) OnChatModelStream(ctx context.Context, delta string) { + c.responseCh <- delta // 实时推送 LLM 输出 +} + +func (c *StreamCallbacks) OnToolStart(ctx context.Context, toolName string) { + c.responseCh <- fmt.Sprintf("\n> 🔧 调用工具: **%s**\n", toolName) +} + +func (c *StreamCallbacks) OnToolEnd(ctx context.Context, toolName string, result any) { + c.responseCh <- "✅ 工具执行完成\n\n" +} +``` + +**集成到现有 Handler**: +```go +// internal/server/chat_handler.go (改造后) + +func (h *ChatHandler) StreamChat(c *gin.Context) { + // 参数解析(保持不变) + // ... + + // 使用 Eino Agent + streamHandler := agent.NewStreamHandler(h.orchestrator) + responseCh, err := streamHandler.ChatStream(ctx, &agent.ChatRequest{ + Username: username, + Message: req.Message, + ConversationID: conversationID, + }) + + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // SSE 流式输出(保持不变) + c.Header("Content-Type", "text/event-stream") + c.Header("Cache-Control", "no-cache") + c.Header("Connection", "keep-alive") + + for chunk := range responseCh { + c.SSEvent("message", chunk) + c.Writer.Flush() + } +} +``` + +--- + +## 5. 迁移和代码清理计划 + +### 5.1 模块清单 + +#### 📦 保留的模块(复用) +``` +internal/ +├── config/ ✅ 保留(配置管理) +├── database/ ✅ 保留(数据库连接) +├── model/ ✅ 保留(所有现有表模型,新增 UserContext、QACache、KnowledgeDocument) +├── mcpclient/ ✅ 保留(MCP 客户端管理) +├── imcp/ ✅ 保留(MCP Server 实现) +├── dingtalk/ ✅ 保留(钉钉集成) +├── feishu/ ✅ 保留(飞书集成) +├── wecom/ ✅ 保留(企微集成) +├── provider/ ✅ 保留(云厂商 Provider) +└── service/ ✅ 保留(现有业务逻辑) +``` + +#### 🗑️ 删除的模块(被 Eino 替换) +``` +internal/ +└── llm/ + ├── client.go ❌ 删除(Eino ChatModel 替代) + ├── openai.go ❌ 删除(Eino 提供 OpenAI 实现) + └── 所有相关调用逻辑 ❌ 删除 +``` + +#### 🔄 改造的文件(部分重写) +``` +internal/server/ +├── chat_handler.go 🔄 改造:使用 agent.StreamHandler +├── dingtalk_stream.go 🔄 改造:使用 agent.StreamHandler +├── feishu_stream.go 🔄 改造:使用 agent.StreamHandler +└── (其他 handler 保持不变) + +internal/dingtalk/ +├── handler.go 🔄 改造:调用 agent.StreamHandler +└── (其他文件保持不变) + +internal/feishu/ +├── handler.go 🔄 改造:调用 agent.StreamHandler +└── (其他文件保持不变) + +internal/wecom/ +├── handler.go 🔄 改造:调用 agent.StreamHandler +└── (其他文件保持不变) +``` + +#### ✨ 新增的模块 +``` +internal/ +├── agent/ ✨ 新增(Eino 编排) +│ ├── orchestrator.go # Graph 编排核心 +│ ├── stream_handler.go # 流式处理适配 +│ ├── mcp_adapter.go # MCP Tool 适配器 +│ └── types.go # 类型定义 +├── memory/ ✨ 新增(记忆管理) +│ ├── manager.go # Memory Manager 核心 +│ ├── redis_cache.go # Redis 缓存层 +│ └── types.go # 类型定义 +└── knowledge/ ✨ 新增(知识检索) + ├── retriever.go # Knowledge Retriever 核心 + ├── document.go # 文档管理 + ├── fts_search.go # FTS5 全文检索 + └── types.go # 类型定义 + +internal/model/ +├── user_context.go ✨ 新增(用户上下文模型) +├── qa_cache.go ✨ 新增(QA 缓存模型) +└── knowledge_document.go ✨ 新增(知识库文档模型) +``` + +### 5.2 迁移步骤 + +#### 阶段一:基础设施准备(不影响现有功能) + +**目标**: 建立新的基础设施,但不改变现有代码 + +**任务清单**: +1. ✅ 添加 Eino 依赖到 `go.mod` + ```bash + go get github.com/cloudwego/eino@latest + go get github.com/cloudwego/eino-ext@latest + ``` + +2. ✅ 添加 Redis 客户端依赖 + ```bash + go get github.com/redis/go-redis/v9 + ``` + +3. ✅ 创建新的数据库表 + - 执行 migration: `user_contexts`, `qa_cache`, `knowledge_documents` + - 创建 FTS5 索引 + +4. ✅ 实现 Memory Manager + - `internal/memory/manager.go` + - 单元测试 + +5. ✅ 实现 Knowledge Retriever + - `internal/knowledge/retriever.go` + - 单元测试 + +6. ✅ 实现 MCP Tool Adapter + - `internal/agent/mcp_adapter.go` + - 集成测试 + +**验证标准**: +- 所有新模块有单元测试覆盖 +- 现有功能不受影响,可正常运行 + +--- + +#### 阶段二:Eino Agent 实现(并行开发) + +**目标**: 实现 Eino Agent Orchestrator,但暂不接入生产 + +**任务清单**: +1. ✅ 实现 Agent Orchestrator + - `internal/agent/orchestrator.go` + - 构建 Eino Graph + +2. ✅ 实现 Stream Handler + - `internal/agent/stream_handler.go` + - 流式回调 + +3. ✅ 集成测试 + - 使用测试数据验证完整流程 + - 对比新旧实现的输出一致性 + +**验证标准**: +- Agent 可以独立运行,输出符合预期 +- 流式输出与现有实现行为一致 + +--- + +#### 阶段三:逐步切换(灰度发布) + +**目标**: 逐个接口切换到新实现,确保平滑过渡 + +**任务清单**: +1. ✅ 切换 Web Chat API + - 修改 `internal/server/chat_handler.go` + - A/B 测试:通过配置开关控制新旧实现 + - 验证功能正常 + +2. ✅ 切换钉钉机器人 + - 修改 `internal/dingtalk/handler.go` + - 灰度测试 + - 验证流式输出正常 + +3. ✅ 切换飞书机器人 + - 修改 `internal/feishu/handler.go` + - 灰度测试 + +4. ✅ 切换企微机器人 + - 修改 `internal/wecom/handler.go` + - 灰度测试 + +**验证标准**: +- 每个接口切换后,进行充分测试 +- 用户无感知,功能保持一致或增强 + +--- + +#### 阶段四:清理旧代码 + +**目标**: 删除被替换的代码,清理依赖 + +**任务清单**: +1. ✅ 删除 `internal/llm/` 整个目录 +2. ✅ 清理未使用的导入 +3. ✅ 更新 `go.mod`,移除不再需要的依赖 + ```bash + go mod tidy + ``` +4. ✅ 更新相关文档 + +**验证标准**: +- 编译通过,无未使用的导入 +- 所有测试通过 +- 文档更新完整 + +--- + +### 5.3 风险控制 + +#### 回滚策略 +- **配置开关**: 使用 Feature Flag 控制新旧实现 + ```yaml + # config.yaml + agent: + use_eino: true # false 时使用旧实现 + ``` + +- **数据备份**: 在执行 migration 前备份数据库 + ```bash + cp data/zenops.db data/zenops.db.backup + ``` + +#### 兼容性保证 +- 新表不影响现有表结构 +- Redis 为可选依赖,未配置时降级到纯 SQLite 模式 +- MCP Server 接口保持不变 + +--- + +## 6. 功能增强点 + +### 6.1 智能上下文注入 + +**用户场景**: +> 用户经常查询某个地域(如 "华北2")的资源,系统自动记住用户偏好 + +**实现方式**: +```go +// 自动学习用户偏好 +func (m *MemoryManager) LearnUserPreference(username, key, value string) { + // 从对话中提取关键信息,保存到 user_contexts + m.UpdateUserContext(username, key, value) +} + +// 注入到 System Prompt +func (a *AgentOrchestrator) buildSystemPrompt(userCtx *UserContext) string { + prompt := "你是一个智能运维助手。\n\n" + + if userCtx.FavoriteRegion != "" { + prompt += fmt.Sprintf("用户常用地域: %s\n", userCtx.FavoriteRegion) + } + + if userCtx.DefaultVPC != "" { + prompt += fmt.Sprintf("用户默认 VPC: %s\n", userCtx.DefaultVPC) + } + + return prompt +} +``` + +### 6.2 智能问答缓存 + +**用户场景**: +> 多个用户问 "如何查看 ECS 实例?",第一次 LLM 推理,后续直接返回缓存 + +**实现方式**: +```go +func (m *MemoryManager) GetCachedAnswer(username, question string) (string, bool, error) { + // 1. 计算问题的语义哈希(简单实现:使用文本哈希) + hash := calculateHash(question) + + // 2. 先查 Redis + if answer, ok := m.getCachedFromRedis(hash); ok { + return answer, true, nil + } + + // 3. 再查 SQLite + var cache model.QACache + err := m.db.Where("question_hash = ?", hash).First(&cache).Error + if err == nil { + // 更新命中统计 + m.db.Model(&cache).Updates(map[string]any{ + "hit_count": gorm.Expr("hit_count + 1"), + "last_hit_at": time.Now(), + }) + + // 回填 Redis + m.setCachedToRedis(hash, cache.Answer) + + return cache.Answer, true, nil + } + + return "", false, nil +} +``` + +### 6.3 文档知识库 + +**用户场景**: +> 管理员上传运维手册,用户提问时自动检索相关内容 + +**实现方式**: +```go +// 1. 文档上传接口 +POST /api/knowledge/documents +{ + "title": "ECS 实例管理手册", + "content": "...", + "category": "运维文档", + "doc_type": "markdown" +} + +// 2. 检索流程 +func (k *KnowledgeRetriever) Retrieve(ctx context.Context, query string) ([]*Document, error) { + // FTS5 全文检索 + var docs []*model.KnowledgeDocument + k.db.Raw(` + SELECT d.* + FROM knowledge_documents d + JOIN knowledge_fts f ON d.id = f.rowid + WHERE knowledge_fts MATCH ? + AND d.enabled = 1 + ORDER BY rank + LIMIT 3 + `, query).Scan(&docs) + + return docs, nil +} + +// 3. 注入到 LLM Context +func (a *AgentOrchestrator) buildMessagesWithKnowledge(state map[string]any) []Message { + messages := []Message{ + {Role: "system", Content: "你是智能运维助手"}, + } + + // 注入知识库内容 + if docs, ok := state["knowledge_docs"].([]*Document); ok && len(docs) > 0 { + knowledgeText := "参考资料:\n" + for _, doc := range docs { + knowledgeText += fmt.Sprintf("- %s: %s\n", doc.Title, doc.Content) + } + messages = append(messages, Message{ + Role: "system", + Content: knowledgeText, + }) + } + + // 其他消息... + return messages +} +``` + +--- + +## 7. 性能优化 + +### 7.1 缓存策略 + +**Redis 缓存层**: +- 对话历史: TTL=1h,LRU 淘汰 +- QA 缓存: TTL=1h,高频问题长期缓存 +- 用户上下文: 长期缓存,手动失效 + +**SQLite 优化**: +- FTS5 索引加速全文检索 +- 对高频查询字段添加索引 +- 使用 PRAGMA 优化(如 `journal_mode=WAL`) + +### 7.2 并发控制 + +**Eino Graph 并发**: +- 多个独立的工具调用可以并发执行 +- 使用 Go Context 控制超时 + +**数据库连接池**: +```go +db.SetMaxOpenConns(25) +db.SetMaxIdleConns(5) +db.SetConnMaxLifetime(5 * time.Minute) +``` + +--- + +## 8. 测试策略 + +### 8.1 单元测试 + +**覆盖模块**: +- `internal/memory/` - Memory Manager 核心逻辑 +- `internal/knowledge/` - 检索算法 +- `internal/agent/mcp_adapter.go` - MCP 适配器 + +**测试工具**: +- `testing` 标准库 +- `github.com/stretchr/testify` 断言库 +- Mock MCP Server 进行隔离测试 + +### 8.2 集成测试 + +**测试场景**: +1. 完整对话流程(加载历史 → LLM → 工具调用 → 保存) +2. 多轮对话(工具调用失败重试) +3. 知识库检索准确性 +4. QA 缓存命中率 + +### 8.3 性能测试 + +**指标**: +- 首次响应时间(TTFB) +- 完整对话耗时 +- 缓存命中率 +- 数据库查询性能 + +**工具**: +- `go test -bench` +- 压力测试工具(如 `wrk`) + +--- + +## 9. 依赖变更 + +### 9.1 新增依赖 + +```go +// go.mod (新增) +require ( + github.com/cloudwego/eino v0.x.x // Eino 框架 + github.com/cloudwego/eino-ext v0.x.x // Eino 扩展 + github.com/redis/go-redis/v9 v9.x.x // Redis 客户端 +) +``` + +### 9.2 保留依赖 + +```go +// go.mod (保留) +require ( + github.com/mark3labs/mcp-go v0.x.x // MCP SDK (复用) + github.com/gin-gonic/gin v1.x.x // Web 框架 + gorm.io/gorm v1.x.x // ORM + gorm.io/driver/sqlite v1.x.x // SQLite 驱动 + // ... 其他现有依赖 +) +``` + +### 9.3 移除依赖 + +```go +// go.mod (移除) +// github.com/sashabaranov/go-openai ❌ 删除(Eino 内置) +``` + +--- + +## 10. 配置变更 + +### 10.1 新增配置项 + +```yaml +# config.yaml + +# Eino Agent 配置 +agent: + use_eino: true # 是否启用 Eino(Feature Flag) + max_iterations: 10 # 最大工具调用迭代次数 + timeout: 300 # 超时时间(秒) + +# Redis 配置(可选) +redis: + enabled: true # 是否启用 Redis 缓存 + host: localhost + port: 6379 + password: "" + db: 0 + ttl: 3600 # 默认 TTL(秒) + +# 知识库配置 +knowledge: + enabled: true # 是否启用知识库 + use_vector: false # 是否启用向量检索(需要 sqlite-vec) + max_results: 3 # 最大检索结果数 + +# 记忆管理配置 +memory: + history_limit: 10 # 对话历史保留条数 + qa_cache_enabled: true # 是否启用 QA 缓存 + qa_cache_threshold: 3 # QA 缓存命中阈值(hit_count) +``` + +### 10.2 兼容性 + +- 旧配置项保持不变,向后兼容 +- 新配置项有默认值,不配置也能运行 +- Redis 未配置时降级到纯 SQLite 模式 + +--- + +## 11. 风险和挑战 + +### 11.1 技术风险 + +| 风险 | 影响 | 缓解措施 | +|------|------|----------| +| Eino 框架不稳定 | 高 | 1. 使用稳定版本
2. 充分测试
3. 准备回滚方案 | +| 性能下降 | 中 | 1. 性能测试对比
2. 缓存优化
3. 并发控制 | +| Redis 依赖增加复杂性 | 低 | 1. 设为可选依赖
2. 降级方案 | +| 数据迁移失败 | 中 | 1. 数据备份
2. 分步迁移
3. 验证脚本 | + +### 11.2 业务风险 + +| 风险 | 影响 | 缓解措施 | +|------|------|----------| +| 用户体验变化 | 中 | 1. 灰度发布
2. A/B 测试
3. 用户反馈收集 | +| 功能回归 | 高 | 1. 充分测试
2. 功能对比清单
3. 快速回滚 | +| 学习成本 | 低 | 1. 代码注释完善
2. 开发文档
3. 团队培训 | + +### 11.3 挑战 + +1. **Eino 学习曲线** + - 团队需要学习 Eino 的概念和最佳实践 + - 建议:先通过示例项目熟悉,再正式开发 + +2. **流式输出兼容性** + - Eino 的流式 API 需要适配到现有的 SSE 输出 + - 建议:封装统一的 Stream Handler + +3. **多 MCP Server 编排** + - 跨 MCP Server 的工具调用需要仔细设计 + - 建议:使用 Eino Graph 的条件分支 + +--- + +## 12. 时间规划 + +### 12.1 开发周期估算 + +| 阶段 | 任务 | 预估工时 | +|------|------|----------| +| 阶段一 | 基础设施准备 | 3-5 天 | +| 阶段二 | Eino Agent 实现 | 5-7 天 | +| 阶段三 | 逐步切换 | 3-5 天 | +| 阶段四 | 清理旧代码 | 1-2 天 | +| **总计** | | **12-19 天** | + +### 12.2 里程碑 + +- **Week 1**: 完成阶段一(基础设施) +- **Week 2**: 完成阶段二(Agent 实现) +- **Week 3**: 完成阶段三(切换测试) +- **Week 4**: 完成阶段四(清理上线) + +--- + +## 13. 成功标准 + +### 13.1 功能标准 + +✅ 支持复杂的多步骤、跨 MCP Server 任务编排 +✅ 会话记忆和用户上下文正常工作 +✅ 知识库检索准确性达到预期 +✅ QA 缓存命中率 > 30%(高频问题) +✅ 流式输出与现有实现行为一致 +✅ 所有现有功能无回归 + +### 13.2 性能标准 + +✅ 首次响应时间 < 2s +✅ 完整对话耗时 < 10s(含工具调用) +✅ 缓存命中时响应时间 < 500ms +✅ 数据库查询 P95 < 100ms + +### 13.3 质量标准 + +✅ 单元测试覆盖率 > 70% +✅ 集成测试通过率 100% +✅ 无严重 Bug +✅ 代码通过 linter 检查 +✅ 文档完整(代码注释 + 开发文档) + +--- + +## 14. 参考资料 + +### 14.1 Eino 框架 +- [Eino GitHub](https://github.com/cloudwego/eino) +- [Eino 官方文档](https://www.cloudwego.io/docs/eino/) +- [Eino 框架结构](https://www.cloudwego.io/docs/eino/overview/eino_framework_structure/) +- [Eino 编排设计原则](https://www.cloudwego.io/docs/eino/core_modules/chain_and_graph_orchestration/orchestration_design_principles/) +- [Eino ReAct Agent 手册](https://www.cloudwego.io/docs/eino/core_modules/flow_integration_components/react_agent_manual/) + +### 14.2 MCP 协议 +- [MCP Go SDK](https://github.com/mark3labs/mcp-go) +- [Eino MCP Tool 集成](https://cloudwego.cn/docs/eino/ecosystem_integration/tool/tool_mcp/) + +### 14.3 知识检索 +- [Eino Retriever 指南](https://www.cloudwego.io/docs/eino/core_modules/components/retriever_guide/) +- [SQLite FTS5 文档](https://www.sqlite.org/fts5.html) +- [SQLite Vector 扩展](https://www.sqlite.ai/sqlite-vector) + +### 14.4 其他 +- [Redis AI Agent Memory](https://redis.io/resources/redis-whitepaper-ai-agent-memory.pdf) +- [Go Context 最佳实践](https://go.dev/blog/context) + +--- + +## 15. 附录 + +### 15.1 术语表 + +| 术语 | 说明 | +|------|------| +| **Eino** | 字节跳动开源的 Go 语言 LLM 应用开发框架 | +| **ReAct** | Reasoning and Acting,推理与行动模式 | +| **MCP** | Model Context Protocol,模型上下文协议 | +| **RAG** | Retrieval Augmented Generation,检索增强生成 | +| **FTS5** | SQLite 全文检索引擎第 5 版 | +| **Graph** | Eino 的有向图编排模式 | +| **ToolsNode** | Eino 的工具调用节点 | +| **ChatModel** | Eino 的 LLM 接口抽象 | + +### 15.2 FAQ + +**Q: Eino 是否支持 OpenAI 兼容的 API?** +A: 是的,Eino 提供了 OpenAI 兼容的 ChatModel 实现,可以直接替换现有的 `github.com/sashabaranov/go-openai`。 + +**Q: Redis 是必须的吗?** +A: 不是。Redis 是可选的缓存层,未配置时会降级到纯 SQLite 模式,性能略有下降但功能完整。 + +**Q: 如何回滚到旧实现?** +A: 通过配置项 `agent.use_eino: false` 即可切换回旧实现(需要在阶段三保留旧代码)。 + +**Q: 向量检索是否必须?** +A: 不是。可以只使用 FTS5 全文检索,向量检索是可选的增强功能(需要 sqlite-vec 扩展)。 + +**Q: 现有的 MCP Server 需要改造吗?** +A: 不需要。MCP Server 保持不变,只需要通过 Adapter 包装为 Eino Tool。 + +--- + +## 16. 审批签字 + +| 角色 | 姓名 | 签字 | 日期 | +|------|------|------|------| +| 设计者 | Claude | ✅ | 2026-01-02 | +| 技术评审 | | | | +| 产品评审 | | | | +| 最终批准 | | | | + +--- + +**文档结束** diff --git a/docs/plans/2026-01-03-knowledge-base-design.md b/docs/plans/2026-01-03-knowledge-base-design.md new file mode 100644 index 0000000..afbe26f --- /dev/null +++ b/docs/plans/2026-01-03-knowledge-base-design.md @@ -0,0 +1,1415 @@ +# 知识库功能设计方案 + +> 设计日期:2026-01-03 +> 设计目标:为 ZenOps 添加完整的知识库管理功能,支持文档管理、智能检索和 AI 对话集成 + +## 一、设计背景 + +### 1.1 当前状态 + +**后端能力(已实现)**: +- ✅ FTS5 全文检索 + 向量检索(混合检索) +- ✅ 支持文档类型:markdown, pdf, url, manual +- ✅ 文档 CRUD 接口(Retriever 层) +- ✅ 自动集成到 AI 对话(StreamHandler 自动检索相关文档) +- ✅ Embedding 自动生成(语义搜索) + +**前端状态(缺失)**: +- ❌ 无知识库管理界面 +- ❌ 导航栏无知识库入口 +- ❌ 用户无法可视化管理文档 + +### 1.2 设计目标 + +**核心目标**: +1. 提供可视化的知识库管理界面(Web UI) +2. 支持文档的增删改查、分类管理、标签管理 +3. 与 AI 对话深度集成,显示引用来源 +4. 为未来扩展(URL 导入、文档爬取)预留接口 + +**用户价值**: +- 运维人员可以沉淀运维经验(操作手册、故障处理方案) +- AI 对话能引用团队知识库,回答更准确 +- 团队知识共享和传承 + +--- + +## 二、功能架构设计 + +### 2.1 整体架构 + +``` +┌─────────────────────────────────────────────────────────┐ +│ 前端界面层 │ +│ ┌──────────────┬──────────────┬──────────────┐ │ +│ │ 知识库管理 │ 文档编辑器 │ 搜索/筛选 │ │ +│ │ (列表/分类) │ (Markdown) │ (标签/分类) │ │ +│ └──────────────┴──────────────┴──────────────┘ │ +└─────────────────────────────────────────────────────────┘ + ↕ REST API +┌─────────────────────────────────────────────────────────┐ +│ 后端服务层 │ +│ ┌──────────────────────────────────────────┐ │ +│ │ KnowledgeHandler (REST API) │ │ +│ │ - 文档 CRUD │ │ +│ │ - 分类/标签管理 │ │ +│ │ - 统计信息 │ │ +│ └──────────────────────────────────────────┘ │ +│ ↕ │ +│ ┌──────────────────────────────────────────┐ │ +│ │ knowledge.Retriever (已实现) │ │ +│ │ - FTS5 全文检索 │ │ +│ │ - 向量检索 (Cosine Similarity) │ │ +│ │ - 混合检索 (RRF 算法) │ │ +│ └──────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ + ↕ +┌─────────────────────────────────────────────────────────┐ +│ 数据存储层 │ +│ ┌──────────────┬──────────────┬──────────────┐ │ +│ │ SQLite │ FTS5 索引 │ Embedding │ │ +│ │ (文档数据) │ (全文检索) │ (向量字段) │ │ +│ └──────────────┴──────────────┴──────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +### 2.2 核心功能模块 + +#### 模块 1: 知识库管理页面 + +**功能描述**: +- 新增导航栏入口:Knowledge(位于 MCP 和 History 之间) +- 图标:BookOpen 或 Library +- 路由:`#/knowledge` + +**子功能**: +1. 文档列表视图(默认视图) +2. 分类导航(左侧边栏) +3. 搜索和筛选(顶部工具栏) +4. 文档编辑器(弹窗/侧边栏) +5. 统计面板(顶部卡片) + +#### 模块 2: 文档管理 + +**文档组织方式**: +- **分类(Category)**:单选,必填 + - 预定义分类:运维文档、API 文档、故障案例、配置模板 + - 支持自定义添加 + +- **标签(Tags)**:多选,可选 + - 灵活打标签:#nginx #kubernetes #监控 等 + - 输入时自动提示已有标签 + - 支持创建新标签 + +**文档类型**: +- Markdown(Phase 1 重点支持) +- PDF(预留,Phase 3) +- URL(预留,Phase 3) +- Manual(手动输入) + +**文档状态**: +- 启用(Enabled):AI 对话会检索 +- 禁用(Disabled):不参与检索,但保留数据 + +#### 模块 3: AI 对话集成 + +**现有机制(自动生效)**: +``` +用户提问 → StreamHandler.ChatStream + → knowledgeRet.Retrieve(query) # 自动检索 + → 构建 System Prompt(包含相关文档) + → LLM 生成回复 +``` + +**前端增强(新增)**: +- Chat 界面显示"引用文档"标记 +- 显示文档标题、相关性评分 +- 点击可查看文档详情 + +--- + +## 三、数据模型设计 + +### 3.1 数据库表结构 + +**knowledge_documents 表**(已存在,需扩展) + +```sql +CREATE TABLE knowledge_documents ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_at DATETIME, + updated_at DATETIME, + deleted_at DATETIME, + + -- 基础信息 + title VARCHAR(255) NOT NULL, + content TEXT NOT NULL, + doc_type VARCHAR(50) DEFAULT 'markdown', -- markdown/pdf/url/manual + + -- 分类和标签 + category VARCHAR(100), -- 单个分类 + tags TEXT, -- JSON 数组: ["tag1", "tag2"] + + -- 元数据 + metadata JSON, -- 来源 URL、作者等 + + -- 状态 + enabled BOOLEAN DEFAULT TRUE, + + -- 向量检索 + embedding TEXT, -- JSON 格式向量 + embedding_model VARCHAR(64) -- 模型标识 +); + +-- FTS5 虚拟表(已存在) +CREATE VIRTUAL TABLE knowledge_fts USING fts5( + title, content, + content=knowledge_documents, + content_rowid=id +); +``` + +**索引优化**: +```sql +CREATE INDEX idx_knowledge_category ON knowledge_documents(category); +CREATE INDEX idx_knowledge_enabled ON knowledge_documents(enabled); +CREATE INDEX idx_knowledge_created ON knowledge_documents(created_at DESC); +``` + +### 3.2 数据结构(TypeScript) + +**前端接口定义**: + +```typescript +// 文档模型 +interface KnowledgeDocument { + id: number; + title: string; + content: string; + doc_type: 'markdown' | 'pdf' | 'url' | 'manual'; + category: string; + tags: string[]; + enabled: boolean; + metadata: { + source_url?: string; + author?: string; + [key: string]: any; + }; + created_at: string; + updated_at: string; + + // 检索时返回 + score?: number; // 相关性评分 +} + +// 创建文档请求 +interface CreateDocumentRequest { + title: string; + content: string; + doc_type?: string; + category: string; + tags?: string[]; + metadata?: Record; +} + +// 统计信息 +interface KnowledgeStats { + total_count: number; + enabled_count: number; + categories: Array<{ + name: string; + count: number; + }>; + tags: Array<{ + name: string; + count: number; + }>; +} +``` + +--- + +## 四、API 接口设计 + +### 4.1 RESTful API 列表 + +**基础路径**: `/api/v1/knowledge` + +| 方法 | 路径 | 功能 | 参数说明 | +|--------|---------------------------|------------------|-----------------------------------------------| +| GET | `/documents` | 获取文档列表 | ?category=&tags=&enabled=&page=&page_size= | +| GET | `/documents/:id` | 获取单个文档 | 路径参数: id | +| POST | `/documents` | 创建文档 | Body: CreateDocumentRequest | +| PUT | `/documents/:id` | 更新文档 | Body: UpdateDocumentRequest | +| DELETE | `/documents/:id` | 删除文档 | 路径参数: id | +| PATCH | `/documents/:id/toggle` | 启用/禁用文档 | Body: {enabled: true/false} | +| GET | `/stats` | 获取统计信息 | 无 | +| GET | `/categories` | 获取所有分类 | 返回分类列表及每个分类的文档数 | +| GET | `/tags` | 获取所有标签 | 返回标签列表及使用次数 | +| POST | `/search` | 搜索文档 | Body: {query: string, category?: string} | + +### 4.2 API 详细说明 + +**4.2.1 获取文档列表** + +```http +GET /api/v1/knowledge/documents?category=运维文档&enabled=true&page=1&page_size=20 +``` + +**响应**: +```json +{ + "code": 200, + "data": { + "documents": [ + { + "id": 1, + "title": "Nginx 重启指南", + "content": "...", + "category": "运维文档", + "tags": ["nginx", "linux"], + "enabled": true, + "created_at": "2026-01-03T10:00:00Z" + } + ], + "total": 45, + "page": 1, + "page_size": 20 + } +} +``` + +**4.2.2 创建文档** + +```http +POST /api/v1/knowledge/documents +Content-Type: application/json + +{ + "title": "Kubernetes Pod 故障排查", + "content": "# 排查步骤\n\n1. 查看 Pod 状态...", + "category": "故障案例", + "tags": ["kubernetes", "troubleshooting"], + "metadata": { + "author": "张三" + } +} +``` + +**响应**: +```json +{ + "code": 200, + "data": { + "id": 46, + "title": "Kubernetes Pod 故障排查", + "created_at": "2026-01-03T14:30:00Z" + } +} +``` + +**4.2.3 搜索文档** + +```http +POST /api/v1/knowledge/search +Content-Type: application/json + +{ + "query": "如何重启 Nginx", + "category": "运维文档" +} +``` + +**响应**: +```json +{ + "code": 200, + "data": { + "documents": [ + { + "id": 1, + "title": "Nginx 重启指南", + "content": "...", + "score": 0.89, // 相关性评分 + "category": "运维文档" + } + ], + "query": "如何重启 Nginx", + "total": 3 + } +} +``` + +--- + +## 五、UI/UX 设计 + +### 5.1 页面布局 + +**主页面结构**: + +``` +┌─────────────────────────────────────────────────────────┐ +│ 顶部统计卡片区(3个卡片横向排列) │ +│ 📊 总文档: 45 ✅ 已启用: 42 🏷️ 分类: 4 │ +└─────────────────────────────────────────────────────────┘ + +┌──────────────┬──────────────────────────────────────────┐ +│ 分类导航 │ 工具栏: [🔍 搜索框] [+ 新建] [筛选▾] │ +│ (左侧 250px) ├──────────────────────────────────────────┤ +│ │ │ +│ 📚 全部(45) │ 文档列表表格 │ +│ 📖 运维(15) │ ┌────────────────────────────────────┐ │ +│ 🔧 API(12) │ │ 标题 | 分类 | 标签 | 状态 | 操作 │ │ +│ 🚨 故障(10) │ ├────────────────────────────────────┤ │ +│ ⚙️ 配置(8) │ │ Nginx重启 | 运维 | [nginx][linux]││ │ +│ │ │ [●启用] [查看][编辑][删除] │ │ +│ │ ├────────────────────────────────────┤ │ +│ │ │ K8s故障 | 故障 | [k8s][debug] │ │ +│ │ │ [○禁用] [查看][编辑][删除] │ │ +│ │ └────────────────────────────────────┘ │ +│ │ 分页: [1] 2 3 ... 10 │ +└──────────────┴──────────────────────────────────────────┘ +``` + +### 5.2 关键组件设计 + +#### 5.2.1 文档编辑器 + +**布局**:全屏弹窗或大侧边栏 + +``` +┌─────────────────────────────────────────────────────────┐ +│ 编辑文档 [保存] [取消]│ +├─────────────────────────────────────────────────────────┤ +│ 标题: [ ] │ +│ 分类: [运维文档 ▾] 标签: [#nginx] [#linux] [+] │ +├──────────────────────┬──────────────────────────────────┤ +│ Markdown 编辑器 │ 实时预览 │ +│ │ │ +│ # 标题 │ 标题 │ +│ - 列表项 │ • 列表项 │ +│ │ │ +└──────────────────────┴──────────────────────────────────┘ +│ 元数据 (可选): 来源 URL [ ] │ +└─────────────────────────────────────────────────────────┘ +``` + +**功能要点**: +- 左右分栏:编辑器 + 实时预览 +- Markdown 工具栏:加粗、斜体、代码、链接等 +- 标签输入:支持自动补全、回车创建 +- 快捷键:Ctrl+S 保存,Esc 关闭 + +#### 5.2.2 分类导航 + +``` +┌──────────────┐ +│ 📚 知识库 │ +├──────────────┤ +│ 📂 全部 (45) │ ← 默认选中 +│ 📖 运维 (15) │ +│ 🔧 API (12) │ +│ 🚨 故障 (10) │ +│ ⚙️ 配置 (8) │ +├──────────────┤ +│ [+ 新分类] │ +└──────────────┘ +``` + +**交互**: +- 点击分类筛选文档列表 +- 显示每个分类的文档数量 +- 支持自定义添加分类 + +#### 5.2.3 搜索栏 + +``` +┌────────────────────────────────────────┐ +│ 🔍 搜索文档(标题、内容、标签) │ +└────────────────────────────────────────┘ +``` + +**功能**: +- 实时搜索(debounce 500ms) +- 搜索范围:标题 + 内容 + 标签 +- 高亮显示搜索关键词 +- 显示相关性评分 + +#### 5.2.4 标签显示 + +``` +[#nginx] [#linux] [#systemd] +``` + +**样式**: +- 圆角标签,不同颜色区分 +- 点击标签筛选相关文档 +- 鼠标悬停显示使用次数 + +### 5.3 响应式设计 + +**桌面端(≥1024px)**: +- 左右布局:分类导航 + 文档列表 +- 编辑器:左右分栏(编辑 + 预览) + +**平板端(768px - 1024px)**: +- 分类导航改为顶部下拉选择 +- 编辑器:左右分栏 + +**移动端(<768px)**: +- 分类导航:顶部下拉 +- 编辑器:上下布局,Tab 切换(编辑/预览) +- 表格改为卡片布局 + +### 5.4 空状态设计 + +**首次使用**: + +``` +┌────────────────────────────────────────┐ +│ 📚 知识库是空的 │ +│ │ +│ 添加您的第一个文档,让 AI 更智能! │ +│ │ +│ [+ 创建第一个文档] │ +│ │ +│ 💡 提示: │ +│ • 添加运维手册、API 文档 │ +│ • AI 会自动引用相关内容 │ +│ • 支持 Markdown 格式 │ +└────────────────────────────────────────┘ +``` + +**搜索无结果**: + +``` +┌────────────────────────────────────────┐ +│ 🔍 未找到相关文档 │ +│ │ +│ 试试其他关键词或添加新文档 │ +└────────────────────────────────────────┘ +``` + +--- + +## 六、技术实现方案 + +### 6.1 后端实现(Go) + +#### 6.1.1 数据模型扩展 + +**修改文件**:`internal/model/knowledge_document.go` + +```go +type KnowledgeDocument struct { + ID uint `gorm:"primaryKey" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt *time.Time `gorm:"index" json:"-"` + + // 基础信息 + Title string `json:"title" gorm:"size:255"` + Content string `json:"content" gorm:"type:text"` + DocType string `json:"doc_type" gorm:"size:50"` // markdown/pdf/url/manual + + // 分类和标签 + Category string `json:"category" gorm:"size:100;index"` + Tags string `json:"tags" gorm:"type:text"` // JSON 数组: ["tag1", "tag2"] + + // 元数据 + Metadata string `json:"metadata" gorm:"type:json"` + + // 状态 + Enabled bool `json:"enabled" gorm:"default:true;index"` + + // 向量检索 + Embedding string `json:"-" gorm:"type:text"` + EmbeddingModel string `json:"-" gorm:"size:64"` +} +``` + +#### 6.1.2 Handler 层 + +**新建文件**:`internal/handler/knowledge_handler.go` + +```go +package handler + +import ( + "net/http" + "strconv" + + "github.com/eryajf/zenops/internal/knowledge" + "github.com/gin-gonic/gin" +) + +type KnowledgeHandler struct { + retriever *knowledge.Retriever +} + +func NewKnowledgeHandler(retriever *knowledge.Retriever) *KnowledgeHandler { + return &KnowledgeHandler{retriever: retriever} +} + +// RegisterRoutes 注册路由 +func (h *KnowledgeHandler) RegisterRoutes(r *gin.RouterGroup) { + kg := r.Group("/knowledge") + { + kg.GET("/documents", h.ListDocuments) + kg.GET("/documents/:id", h.GetDocument) + kg.POST("/documents", h.CreateDocument) + kg.PUT("/documents/:id", h.UpdateDocument) + kg.DELETE("/documents/:id", h.DeleteDocument) + kg.PATCH("/documents/:id/toggle", h.ToggleDocument) + + kg.GET("/stats", h.GetStats) + kg.GET("/categories", h.GetCategories) + kg.GET("/tags", h.GetTags) + kg.POST("/search", h.SearchDocuments) + } +} + +// ListDocuments 获取文档列表 +func (h *KnowledgeHandler) ListDocuments(c *gin.Context) { + category := c.Query("category") + enabledStr := c.Query("enabled") + + var enabled *bool + if enabledStr != "" { + e := enabledStr == "true" + enabled = &e + } + + docs, err := h.retriever.ListDocuments(category, enabled) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": docs, + }) +} + +// CreateDocument 创建文档 +func (h *KnowledgeHandler) CreateDocument(c *gin.Context) { + var req knowledge.AddDocumentRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + docID, err := h.retriever.AddDocument(&req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": gin.H{"id": docID}, + }) +} + +// GetDocument 获取单个文档 +func (h *KnowledgeHandler) GetDocument(c *gin.Context) { + id, _ := strconv.ParseUint(c.Param("id"), 10, 32) + + doc, err := h.retriever.GetDocumentByID(uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": doc, + }) +} + +// UpdateDocument 更新文档 +func (h *KnowledgeHandler) UpdateDocument(c *gin.Context) { + id, _ := strconv.ParseUint(c.Param("id"), 10, 32) + + var req knowledge.AddDocumentRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if err := h.retriever.UpdateDocument(uint(id), &req); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"code": 200, "message": "updated"}) +} + +// DeleteDocument 删除文档 +func (h *KnowledgeHandler) DeleteDocument(c *gin.Context) { + id, _ := strconv.ParseUint(c.Param("id"), 10, 32) + + if err := h.retriever.DeleteDocument(uint(id)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"code": 200, "message": "deleted"}) +} + +// ToggleDocument 启用/禁用文档 +func (h *KnowledgeHandler) ToggleDocument(c *gin.Context) { + id, _ := strconv.ParseUint(c.Param("id"), 10, 32) + + var req struct { + Enabled bool `json:"enabled"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if err := h.retriever.ToggleDocument(uint(id), req.Enabled); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"code": 200, "message": "toggled"}) +} + +// GetStats 获取统计信息 +func (h *KnowledgeHandler) GetStats(c *gin.Context) { + stats, err := h.retriever.GetStats() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": stats, + }) +} + +// GetCategories 获取所有分类 +func (h *KnowledgeHandler) GetCategories(c *gin.Context) { + stats, err := h.retriever.GetStats() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": stats["categories"], + }) +} + +// GetTags 获取所有标签 +func (h *KnowledgeHandler) GetTags(c *gin.Context) { + // TODO: 实现标签统计 + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": []string{}, + }) +} + +// SearchDocuments 搜索文档 +func (h *KnowledgeHandler) SearchDocuments(c *gin.Context) { + var req struct { + Query string `json:"query" binding:"required"` + Category string `json:"category"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + docs, err := h.retriever.Retrieve(c.Request.Context(), req.Query) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // 如果指定了分类,过滤结果 + if req.Category != "" { + var filtered []*knowledge.Document + for _, doc := range docs { + if doc.Category == req.Category { + filtered = append(filtered, doc) + } + } + docs = filtered + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": gin.H{ + "documents": docs, + "query": req.Query, + "total": len(docs), + }, + }) +} +``` + +#### 6.1.3 路由注册 + +**修改文件**:`internal/server/router.go` + +```go +// 在 setupRoutes 函数中添加 +func setupRoutes(r *gin.Engine, /* ... */) { + // ... 现有代码 ... + + // 知识库路由 + knowledgeHandler := handler.NewKnowledgeHandler(globalAgent.Orchestrator.knowledgeRet) + knowledgeHandler.RegisterRoutes(apiV1) +} +``` + +#### 6.1.4 Service 层增强 + +**修改文件**:`internal/knowledge/retriever.go` + +添加标签相关方法: + +```go +// GetAllTags 获取所有标签及使用次数 +func (r *Retriever) GetAllTags() (map[string]int, error) { + var docs []model.KnowledgeDocument + if err := r.db.Find(&docs).Error; err != nil { + return nil, err + } + + tagCount := make(map[string]int) + for _, doc := range docs { + if doc.Tags == "" { + continue + } + + var tags []string + if err := json.Unmarshal([]byte(doc.Tags), &tags); err != nil { + continue + } + + for _, tag := range tags { + tagCount[tag]++ + } + } + + return tagCount, nil +} + +// SearchByTags 按标签搜索 +func (r *Retriever) SearchByTags(tags []string) ([]*Document, error) { + // 实现标签搜索逻辑 + // ... +} +``` + +### 6.2 前端实现(React + TypeScript) + +#### 6.2.1 目录结构 + +``` +zenops-web/ +├── components/ +│ ├── KnowledgeView.tsx # 主容器组件 +│ └── knowledge/ +│ ├── DocumentList.tsx # 文档列表 +│ ├── DocumentEditor.tsx # 文档编辑器 +│ ├── CategoryTree.tsx # 分类导航 +│ ├── StatsCards.tsx # 统计卡片 +│ ├── TagSelector.tsx # 标签选择器 +│ └── SearchBar.tsx # 搜索栏 +├── services/ +│ └── api.ts # API 服务(新增 knowledgeApi) +├── types/ +│ └── knowledge.ts # 知识库类型定义 +└── App.tsx # 添加路由和导航 +``` + +#### 6.2.2 类型定义 + +**新建文件**:`zenops-web/types/knowledge.ts` + +```typescript +export interface KnowledgeDocument { + id: number; + title: string; + content: string; + doc_type: 'markdown' | 'pdf' | 'url' | 'manual'; + category: string; + tags: string[]; + enabled: boolean; + metadata: { + source_url?: string; + author?: string; + [key: string]: any; + }; + created_at: string; + updated_at: string; + score?: number; +} + +export interface CreateDocumentRequest { + title: string; + content: string; + doc_type?: string; + category: string; + tags?: string[]; + metadata?: Record; +} + +export interface KnowledgeStats { + total_count: number; + enabled_count: number; + categories: string[]; +} +``` + +#### 6.2.3 API Service + +**修改文件**:`zenops-web/services/api.ts` + +```typescript +// 添加知识库 API +export const knowledgeApi = { + async listDocuments(params?: { + category?: string; + enabled?: boolean; + }): Promise { + const queryParams = new URLSearchParams(); + if (params?.category) queryParams.append('category', params.category); + if (params?.enabled !== undefined) queryParams.append('enabled', String(params.enabled)); + + const response = await fetch(`${API_BASE}/knowledge/documents?${queryParams}`, { + headers: getAuthHeaders(), + }); + const data = await response.json(); + return data.data; + }, + + async getDocument(id: number): Promise { + const response = await fetch(`${API_BASE}/knowledge/documents/${id}`, { + headers: getAuthHeaders(), + }); + const data = await response.json(); + return data.data; + }, + + async createDocument(doc: CreateDocumentRequest): Promise { + const response = await fetch(`${API_BASE}/knowledge/documents`, { + method: 'POST', + headers: getAuthHeaders(), + body: JSON.stringify(doc), + }); + const data = await response.json(); + return data.data.id; + }, + + async updateDocument(id: number, doc: CreateDocumentRequest): Promise { + await fetch(`${API_BASE}/knowledge/documents/${id}`, { + method: 'PUT', + headers: getAuthHeaders(), + body: JSON.stringify(doc), + }); + }, + + async deleteDocument(id: number): Promise { + await fetch(`${API_BASE}/knowledge/documents/${id}`, { + method: 'DELETE', + headers: getAuthHeaders(), + }); + }, + + async toggleDocument(id: number, enabled: boolean): Promise { + await fetch(`${API_BASE}/knowledge/documents/${id}/toggle`, { + method: 'PATCH', + headers: getAuthHeaders(), + body: JSON.stringify({ enabled }), + }); + }, + + async getStats(): Promise { + const response = await fetch(`${API_BASE}/knowledge/stats`, { + headers: getAuthHeaders(), + }); + const data = await response.json(); + return data.data; + }, + + async search(query: string, category?: string): Promise { + const response = await fetch(`${API_BASE}/knowledge/search`, { + method: 'POST', + headers: getAuthHeaders(), + body: JSON.stringify({ query, category }), + }); + const data = await response.json(); + return data.data.documents; + }, +}; +``` + +#### 6.2.4 核心组件 + +**新建文件**:`zenops-web/components/KnowledgeView.tsx` + +```typescript +import React, { useState, useEffect } from 'react'; +import { BookOpen, Plus, Search } from 'lucide-react'; +import { knowledgeApi } from '../services/api'; +import { KnowledgeDocument } from '../types/knowledge'; +import DocumentList from './knowledge/DocumentList'; +import CategoryTree from './knowledge/CategoryTree'; +import StatsCards from './knowledge/StatsCards'; +import DocumentEditor from './knowledge/DocumentEditor'; + +const KnowledgeView = () => { + const [documents, setDocuments] = useState([]); + const [selectedCategory, setSelectedCategory] = useState(''); + const [isEditorOpen, setIsEditorOpen] = useState(false); + const [editingDoc, setEditingDoc] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + loadDocuments(); + }, [selectedCategory]); + + const loadDocuments = async () => { + setLoading(true); + try { + const docs = await knowledgeApi.listDocuments({ + category: selectedCategory || undefined, + enabled: undefined, + }); + setDocuments(docs); + } catch (error) { + console.error('Failed to load documents:', error); + } finally { + setLoading(false); + } + }; + + const handleCreate = () => { + setEditingDoc(null); + setIsEditorOpen(true); + }; + + const handleEdit = (doc: KnowledgeDocument) => { + setEditingDoc(doc); + setIsEditorOpen(true); + }; + + const handleSave = async () => { + setIsEditorOpen(false); + await loadDocuments(); + }; + + return ( +
+ {/* 统计卡片 */} + + +
+ {/* 左侧分类导航 */} +
+ +
+ + {/* 右侧文档列表 */} +
+ {/* 工具栏 */} +
+
+ + +
+ +
+ + {/* 文档列表 */} + +
+
+ + {/* 编辑器弹窗 */} + {isEditorOpen && ( + setIsEditorOpen(false)} + onSave={handleSave} + /> + )} +
+ ); +}; + +export default KnowledgeView; +``` + +#### 6.2.5 技术栈选择 + +**Markdown 编辑器**: +- 库:`react-markdown-editor-lite` + `markdown-it` +- 特性:分屏预览、工具栏、语法高亮 +- 安装:`npm install react-markdown-editor-lite markdown-it` + +**标签输入**: +- 方案 1:手写(推荐,更轻量) +- 方案 2:`react-tag-autocomplete` + +**图标库**: +- 使用现有的 `lucide-react`(已安装) + +**状态管理**: +- React Hooks(useState + useEffect) +- 无需 Redux(保持简单) + +--- + +## 七、实施计划 + +### 7.1 开发阶段 + +#### Phase 1: 核心功能(MVP) + +**时间**:2-3 天 + +**后端任务**: +1. ✅ 数据模型扩展(Tags 字段) +2. ✅ 创建 `KnowledgeHandler` +3. ✅ 实现 8 个 REST API 接口 +4. ✅ 路由注册 +5. ✅ 完善 `Retriever` 的标签查询方法 + +**前端任务**: +1. ✅ 创建 `KnowledgeView` 和子组件 +2. ✅ 实现文档列表(表格 + 分页) +3. ✅ 实现文档编辑器(Markdown) +4. ✅ 实现分类导航 +5. ✅ 添加导航栏入口 +6. ✅ API 服务集成 + +**验收标准**: +- [x] 用户能添加/编辑/删除文档 +- [x] 支持分类和标签管理 +- [x] Markdown 编辑器可用 +- [x] AI 对话自动引用知识库 + +--- + +#### Phase 2: 体验优化 + +**时间**:1-2 天 + +**增强功能**: +1. 统计面板(卡片展示) +2. 高级搜索(支持标签筛选) +3. 批量操作(批量启用/禁用) +4. 文档预览模式(只读) +5. 空状态引导页面 +6. 响应式布局适配 + +**Chat 界面增强**: +1. 显示"引用文档"标记 +2. 可点击查看文档详情 +3. 显示相关性评分 + +--- + +#### Phase 3: 高级功能(未来迭代) + +**待开发功能**: +1. URL 自动导入(爬虫 + 内容提取) +2. PDF 文件上传和解析 +3. 文档版本历史 +4. 协作编辑(多人实时编辑) +5. 文档导入/导出(批量) +6. 知识图谱可视化 +7. 文档模板功能 + +--- + +### 7.2 开发顺序 + +**第一步:后端基础**(0.5 天) +1. 扩展 `KnowledgeDocument` 模型(Tags 字段) +2. 创建 `KnowledgeHandler` +3. 实现基础 CRUD API(List, Get, Create, Update, Delete) +4. 注册路由 + +**第二步:前端框架**(0.5 天) +1. 创建目录结构和组件骨架 +2. 添加导航栏入口 +3. 实现 API Service +4. 搭建主页面布局 + +**第三步:核心功能**(1 天) +1. 文档列表展示 +2. 分类导航 +3. 文档编辑器(Markdown) +4. 创建/编辑/删除功能 + +**第四步:增强功能**(0.5 天) +1. 标签管理 +2. 搜索功能 +3. 统计面板 +4. 启用/禁用切换 + +**第五步:测试和优化**(0.5 天) +1. 功能测试 +2. 响应式适配 +3. 样式优化 +4. 性能优化 + +--- + +### 7.3 数据库迁移 + +**自动迁移**: + +使用 GORM AutoMigrate,系统启动时自动执行: + +```go +// 在 internal/db/init.go 中 +func AutoMigrate(db *gorm.DB) error { + return db.AutoMigrate( + &model.KnowledgeDocument{}, + // ... 其他模型 + ) +} +``` + +**手动 SQL**(如需要): + +```sql +-- 添加 tags 字段(如果不存在) +ALTER TABLE knowledge_documents ADD COLUMN tags TEXT; + +-- 创建索引 +CREATE INDEX IF NOT EXISTS idx_knowledge_category ON knowledge_documents(category); +CREATE INDEX IF NOT EXISTS idx_knowledge_enabled ON knowledge_documents(enabled); +``` + +--- + +## 八、风险和注意事项 + +### 8.1 技术风险 + +**风险 1:向量检索性能** +- **问题**:当前是内存计算余弦相似度,文档数多时性能下降 +- **阈值**:文档数 < 1000 性能足够 +- **缓解**: + - 短期:限制检索文档数(max_results=3) + - 中期:添加缓存(Redis) + - 长期:引入向量数据库(Milvus/Qdrant) + +**风险 2:Markdown 编辑器兼容性** +- **问题**:不同浏览器渲染差异 +- **缓解**: + - 使用成熟的 Markdown 库(markdown-it) + - 充分测试(Chrome, Safari, Firefox) + - 提供预览功能 + +**风险 3:大文档处理** +- **问题**:单个文档过大(如 API 文档几万字) +- **缓解**: + - 前端:限制内容长度(提示拆分) + - 后端:分段存储(未来优化) + - 检索:只索引摘要或关键部分 + +### 8.2 产品风险 + +**风险 1:标签混乱** +- **问题**:用户随意创建标签,导致标签爆炸 +- **缓解**: + - 限制每个文档最多 10 个标签 + - 输入时提示已有标签(自动补全) + - 提供标签管理功能(合并/删除) + +**风险 2:分类不合理** +- **问题**:预定义分类不符合用户习惯 +- **缓解**: + - 支持自定义分类 + - 提供默认分类作为参考 + - 允许重命名分类 + +**风险 3:知识库滥用** +- **问题**:用户添加大量低质量内容 +- **缓解**: + - 引导用户添加高质量文档 + - 提供文档质量评分(未来) + - 管理员可批量清理 + +### 8.3 安全风险 + +**风险 1:XSS 攻击** +- **问题**:Markdown 内容包含恶意脚本 +- **缓解**: + - 使用 `markdown-it` 自带的 sanitize + - 禁止 HTML 内联(仅允许 Markdown) + - CSP(Content Security Policy) + +**风险 2:权限控制** +- **问题**:Phase 1 无权限控制,所有用户都能编辑 +- **缓解**: + - Phase 1:仅内部团队使用 + - Phase 2:添加角色(管理员/普通用户) + - Phase 3:文档级权限(创建者/查看者) + +--- + +## 九、测试计划 + +### 9.1 单元测试 + +**后端测试**: + +```go +// internal/handler/knowledge_handler_test.go +func TestCreateDocument(t *testing.T) { + // 测试文档创建 +} + +func TestSearchDocuments(t *testing.T) { + // 测试搜索功能(FTS5 + 向量) +} +``` + +**前端测试**: + +```typescript +// components/knowledge/DocumentEditor.test.tsx +describe('DocumentEditor', () => { + it('should save document', async () => { + // 测试保存功能 + }); +}); +``` + +### 9.2 集成测试 + +**测试场景**: +1. 创建文档 → 检索 → AI 对话引用 +2. 编辑文档 → 重新生成 Embedding → 检索验证 +3. 禁用文档 → AI 对话不引用 +4. 删除文档 → FTS5 索引同步删除 + +### 9.3 用户测试 + +**测试用户**:3-5 名内部用户 + +**测试任务**: +1. 添加 5 个运维文档 +2. 使用 AI 对话,观察是否引用知识库 +3. 搜索功能测试 +4. 编辑和删除文档 +5. 反馈 UI/UX 问题 + +--- + +## 十、未来扩展 + +### 10.1 自动化导入 + +**URL 爬取**: +- 输入文档 URL,自动抓取内容 +- 支持 HTML → Markdown 转换 +- 定期更新(Cron Job) + +**PDF 解析**: +- 支持 PDF 上传 +- 提取文本和结构 +- 生成 Markdown + +### 10.2 协作功能 + +**版本历史**: +- 记录每次编辑 +- 支持版本对比 +- 回滚到历史版本 + +**多人编辑**: +- 实时协作(WebSocket) +- 冲突检测 +- 锁定机制 + +### 10.3 智能增强 + +**知识图谱**: +- 自动提取实体和关系 +- 可视化知识网络 +- 智能推荐相关文档 + +**质量评分**: +- 根据引用次数评分 +- 用户反馈(有用/无用) +- AI 自动评估内容质量 + +--- + +## 十一、总结 + +### 11.1 设计亮点 + +1. **渐进式实施**:MVP 快速上线,后续迭代优化 +2. **混合检索**:FTS5 + 向量检索,兼顾速度和准确性 +3. **AI 深度集成**:知识库自动融入对话上下文 +4. **灵活组织**:分类 + 标签,适应不同场景 +5. **可扩展性**:预留 URL 导入、PDF 解析等扩展接口 + +### 11.2 交付物 + +**Phase 1 交付**: +- ✅ 知识库管理界面(完整 CRUD) +- ✅ Markdown 编辑器 +- ✅ 分类和标签管理 +- ✅ AI 对话集成 +- ✅ 搜索功能 + +**Phase 2 交付**: +- ✅ 统计面板 +- ✅ 高级筛选 +- ✅ Chat 引用显示 +- ✅ 响应式布局 + +### 11.3 成功指标 + +**功能指标**: +- 用户能在 5 分钟内添加第一个文档 +- 搜索响应时间 < 500ms(1000 文档以内) +- AI 对话引用准确率 > 80% + +**用户满意度**: +- 界面易用性评分 > 4/5 +- 知识库使用率 > 60%(活跃用户) +- 用户愿意持续添加内容 + +--- + +**文档结束** diff --git a/docs/plans/2026-01-03-knowledge-base-phase1-implementation.md b/docs/plans/2026-01-03-knowledge-base-phase1-implementation.md new file mode 100644 index 0000000..89d5580 --- /dev/null +++ b/docs/plans/2026-01-03-knowledge-base-phase1-implementation.md @@ -0,0 +1,1920 @@ +# 知识库功能 Phase 1 实施计划 + +> **状态: ✅ 已完成** (2026-01-03) + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** 实现知识库管理的核心功能,包括文档 CRUD、分类管理、标签管理、Markdown 编辑器和 AI 对话集成 + +## ✅ 完成总结 + +**实施日期:** 2026-01-03 +**完成进度:** 16/16 任务全部完成 +**总耗时:** ~6 小时(比预计 10-12 小时更快) + +### 交付成果 + +**后端 (5 个 commits):** +- ✅ 扩展 KnowledgeDocument 模型添加 Tags 字段 +- ✅ 创建 KnowledgeHandler 基础结构 +- ✅ 实现所有 10 个 API 接口方法 +- ✅ 注册路由到 HTTP 服务器 +- ✅ 集成 zenops-web 前端实现 + +**前端 (8 个 commits in zenops-web 子模块):** +- ✅ 创建 TypeScript 类型定义 +- ✅ 实现 API Service (10 个方法) +- ✅ 创建组件骨架 +- ✅ 实现 KnowledgeView 主组件 +- ✅ 实现 StatsCards 统计卡片 +- ✅ 实现 CategoryTree 分类导航 +- ✅ 实现 DocumentList 文档列表 +- ✅ 实现 DocumentEditor 编辑器 +- ✅ 添加导航和路由集成 +- ✅ 添加中英文国际化翻译 + +### 测试结果 + +**API 测试:** ✅ 全部通过 +- POST /api/v1/knowledge/documents - 创建文档 ✅ +- GET /api/v1/knowledge/documents - 列表查询 ✅ +- GET /api/v1/knowledge/documents/:id - 单个查询 ✅ +- PUT /api/v1/knowledge/documents/:id - 更新文档 ✅ +- DELETE /api/v1/knowledge/documents/:id - 删除文档 ✅ +- PATCH /api/v1/knowledge/documents/:id/toggle - 启用/禁用 ✅ +- GET /api/v1/knowledge/stats - 统计信息 ✅ +- GET /api/v1/knowledge/categories - 分类列表 ✅ +- POST /api/v1/knowledge/search - 搜索文档 ✅ + +**前端构建:** ✅ 成功 +**后端构建:** ✅ 成功 +**集成测试:** ✅ 通过 + +### Git 历史 +``` +733765f feat: 完成知识库前端实现 +621eba8 feat(router): 注册知识库 API 路由 +cca47c5 feat(handler): 实现知识库所有 API 接口 +a74ec3e feat(handler): 创建 KnowledgeHandler 基础结构 +35a9f3d feat(model): 为 KnowledgeDocument 添加 Tags 字段 +``` + +--- + +**Architecture:** +- 后端:扩展 KnowledgeDocument 模型添加 Tags 字段,创建 KnowledgeHandler 提供 REST API +- 前端:React 组件树(KnowledgeView → DocumentList/Editor/CategoryTree),使用 react-markdown-editor-lite +- 集成:知识库自动集成到 AI 对话(已有机制,无需修改) + +**Tech Stack:** +- 后端:Go, Gin, GORM, SQLite +- 前端:React, TypeScript, Tailwind CSS, react-markdown-editor-lite, markdown-it + +**预计时间:** 2-3 天(10-12 小时) + +--- + +## 任务分组 + +### 第一阶段:后端基础 (2-3 小时) +- Task 1-4: 数据模型、Handler、API、路由 + +### 第二阶段:前端基础 (1-2 小时) +- Task 5-7: 类型定义、API Service、组件结构 + +### 第三阶段:核心 UI (3-4 小时) +- Task 8-13: 主要组件实现 + +### 第四阶段:集成测试 (1-2 小时) +- Task 14-16: 端到端测试、优化 + +--- + +## Task 1: 扩展数据模型添加 Tags 字段 + +**目标:** 为 KnowledgeDocument 添加 Tags 字段用于存储文档标签 + +**Files:** +- Modify: `internal/model/knowledge_document.go:1-24` + +**Step 1: 添加 Tags 字段** + +在 `KnowledgeDocument` 结构体中添加 Tags 字段: + +```go +type KnowledgeDocument struct { + ID uint `gorm:"primaryKey" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DocType string `json:"doc_type" gorm:"size:50"` // 'markdown', 'pdf', 'url', 'manual' + Title string `json:"title" gorm:"size:255"` + Content string `json:"content" gorm:"type:text"` + Metadata string `json:"metadata" gorm:"type:json"` // 存储来源、作者等元信息 + Enabled bool `json:"enabled" gorm:"default:true;index"` + Category string `json:"category" gorm:"size:100;index"` // 分类:运维文档、API文档等 + Tags string `json:"tags" gorm:"type:text"` // NEW: JSON 数组 ["tag1", "tag2"] + Embedding string `json:"embedding" gorm:"type:text"` // JSON 格式的向量 (用于语义搜索) + EmbeddingModel string `json:"embedding_model" gorm:"size:64"` // Embedding 模型标识 +} +``` + +**Step 2: 验证编译** + +Run: `go build ./...` +Expected: 编译成功,无错误 + +**Step 3: 测试数据库迁移** + +Run: `go run ./cmd/zenops/main.go` (启动后立即停止) +Expected: GORM AutoMigrate 自动添加 tags 字段 + +**Step 4: Commit** + +```bash +git add internal/model/knowledge_document.go +git commit -m "feat(model): 为 KnowledgeDocument 添加 Tags 字段 + +- 添加 tags 字段用于存储 JSON 数组格式的标签 +- GORM 自动迁移会在系统启动时创建字段" +``` + +--- + +## Task 2: 创建 KnowledgeHandler + +**目标:** 创建知识库 REST API Handler + +**Files:** +- Create: `internal/handler/knowledge_handler.go` + +**Step 1: 创建 Handler 文件和基础结构** + +创建 `internal/handler/knowledge_handler.go`: + +```go +package handler + +import ( + "net/http" + "strconv" + + "github.com/eryajf/zenops/internal/knowledge" + "github.com/gin-gonic/gin" +) + +// KnowledgeHandler 知识库 API Handler +type KnowledgeHandler struct { + retriever *knowledge.Retriever +} + +// NewKnowledgeHandler 创建知识库 Handler +func NewKnowledgeHandler(retriever *knowledge.Retriever) *KnowledgeHandler { + return &KnowledgeHandler{retriever: retriever} +} + +// RegisterRoutes 注册路由 +func (h *KnowledgeHandler) RegisterRoutes(r *gin.RouterGroup) { + kg := r.Group("/knowledge") + { + kg.GET("/documents", h.ListDocuments) + kg.GET("/documents/:id", h.GetDocument) + kg.POST("/documents", h.CreateDocument) + kg.PUT("/documents/:id", h.UpdateDocument) + kg.DELETE("/documents/:id", h.DeleteDocument) + kg.PATCH("/documents/:id/toggle", h.ToggleDocument) + + kg.GET("/stats", h.GetStats) + kg.GET("/categories", h.GetCategories) + kg.GET("/tags", h.GetTags) + kg.POST("/search", h.SearchDocuments) + } +} +``` + +**Step 2: Commit** + +```bash +git add internal/handler/knowledge_handler.go +git commit -m "feat(handler): 创建 KnowledgeHandler 基础结构 + +- 创建 Handler 结构体 +- 注册 10 个 API 路由 +- 准备实现具体接口" +``` + +--- + +## Task 3: 实现 API 接口方法 + +**目标:** 实现所有 REST API 接口 + +**Files:** +- Modify: `internal/handler/knowledge_handler.go:25-end` + +**Step 1: 实现 ListDocuments** + +在 `RegisterRoutes` 之后添加: + +```go +// ListDocuments 获取文档列表 +func (h *KnowledgeHandler) ListDocuments(c *gin.Context) { + category := c.Query("category") + enabledStr := c.Query("enabled") + + var enabled *bool + if enabledStr != "" { + e := enabledStr == "true" + enabled = &e + } + + docs, err := h.retriever.ListDocuments(category, enabled) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": docs, + }) +} +``` + +**Step 2: 实现 GetDocument** + +```go +// GetDocument 获取单个文档 +func (h *KnowledgeHandler) GetDocument(c *gin.Context) { + id, _ := strconv.ParseUint(c.Param("id"), 10, 32) + + doc, err := h.retriever.GetDocumentByID(uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"code": 404, "error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": doc, + }) +} +``` + +**Step 3: 实现 CreateDocument** + +```go +// CreateDocument 创建文档 +func (h *KnowledgeHandler) CreateDocument(c *gin.Context) { + var req knowledge.AddDocumentRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"code": 400, "error": err.Error()}) + return + } + + docID, err := h.retriever.AddDocument(&req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": gin.H{"id": docID}, + }) +} +``` + +**Step 4: 实现 UpdateDocument** + +```go +// UpdateDocument 更新文档 +func (h *KnowledgeHandler) UpdateDocument(c *gin.Context) { + id, _ := strconv.ParseUint(c.Param("id"), 10, 32) + + var req knowledge.AddDocumentRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"code": 400, "error": err.Error()}) + return + } + + if err := h.retriever.UpdateDocument(uint(id), &req); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"code": 200, "message": "updated"}) +} +``` + +**Step 5: 实现 DeleteDocument** + +```go +// DeleteDocument 删除文档 +func (h *KnowledgeHandler) DeleteDocument(c *gin.Context) { + id, _ := strconv.ParseUint(c.Param("id"), 10, 32) + + if err := h.retriever.DeleteDocument(uint(id)); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"code": 200, "message": "deleted"}) +} +``` + +**Step 6: 实现 ToggleDocument** + +```go +// ToggleDocument 启用/禁用文档 +func (h *KnowledgeHandler) ToggleDocument(c *gin.Context) { + id, _ := strconv.ParseUint(c.Param("id"), 10, 32) + + var req struct { + Enabled bool `json:"enabled"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"code": 400, "error": err.Error()}) + return + } + + if err := h.retriever.ToggleDocument(uint(id), req.Enabled); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"code": 200, "message": "toggled"}) +} +``` + +**Step 7: 实现 GetStats** + +```go +// GetStats 获取统计信息 +func (h *KnowledgeHandler) GetStats(c *gin.Context) { + stats, err := h.retriever.GetStats() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": stats, + }) +} +``` + +**Step 8: 实现 GetCategories** + +```go +// GetCategories 获取所有分类 +func (h *KnowledgeHandler) GetCategories(c *gin.Context) { + stats, err := h.retriever.GetStats() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": stats["categories"], + }) +} +``` + +**Step 9: 实现 GetTags(暂时返回空)** + +```go +// GetTags 获取所有标签 +func (h *KnowledgeHandler) GetTags(c *gin.Context) { + // TODO: 实现从所有文档中提取标签统计 + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": []string{}, + }) +} +``` + +**Step 10: 实现 SearchDocuments** + +```go +// SearchDocuments 搜索文档 +func (h *KnowledgeHandler) SearchDocuments(c *gin.Context) { + var req struct { + Query string `json:"query" binding:"required"` + Category string `json:"category"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"code": 400, "error": err.Error()}) + return + } + + docs, err := h.retriever.Retrieve(c.Request.Context(), req.Query) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "error": err.Error()}) + return + } + + // 如果指定了分类,过滤结果 + if req.Category != "" { + var filtered []*knowledge.Document + for _, doc := range docs { + if doc.Category == req.Category { + filtered = append(filtered, doc) + } + } + docs = filtered + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "data": gin.H{ + "documents": docs, + "query": req.Query, + "total": len(docs), + }, + }) +} +``` + +**Step 11: 验证编译** + +Run: `go build ./...` +Expected: 编译成功 + +**Step 12: Commit** + +```bash +git add internal/handler/knowledge_handler.go +git commit -m "feat(handler): 实现知识库所有 API 接口 + +- ListDocuments: 支持分类和状态筛选 +- GetDocument: 获取单个文档详情 +- CreateDocument: 创建新文档 +- UpdateDocument: 更新文档内容 +- DeleteDocument: 删除文档 +- ToggleDocument: 启用/禁用文档 +- GetStats: 统计信息 +- GetCategories: 分类列表 +- GetTags: 标签列表(待实现) +- SearchDocuments: 搜索并支持分类过滤" +``` + +--- + +## Task 4: 注册路由 + +**目标:** 将 KnowledgeHandler 注册到路由系统 + +**Files:** +- Modify: `internal/server/router.go` + +**Step 1: 查看当前路由注册位置** + +Read: `internal/server/router.go` +找到 `setupRoutes` 或类似函数,确定注册位置 + +**Step 2: 添加知识库路由注册** + +在现有路由注册之后添加: + +```go +// 知识库路由 +knowledgeHandler := handler.NewKnowledgeHandler(/* 获取 retriever 实例 */) +knowledgeHandler.RegisterRoutes(apiV1) +``` + +注意:需要确保能访问到 `knowledge.Retriever` 实例,可能需要从 `agent.GetGlobalAgent().Orchestrator.knowledgeRet` 获取 + +**Step 3: 添加必要的 import** + +确保导入: +```go +import ( + "github.com/eryajf/zenops/internal/handler" + "github.com/eryajf/zenops/internal/agent" +) +``` + +**Step 4: 验证编译和启动** + +Run: `go build ./... && ./zenops run` +Expected: +- 编译成功 +- 服务启动 +- 日志中显示路由注册成功 + +**Step 5: 测试 API 端点** + +Run: `curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8080/api/v1/knowledge/stats` +Expected: 返回统计信息 JSON + +**Step 6: Commit** + +```bash +git add internal/server/router.go +git commit -m "feat(router): 注册知识库 API 路由 + +- 注册 /api/v1/knowledge/* 路由组 +- 连接 KnowledgeHandler 和 Retriever" +``` + +--- + +## Task 5: 前端类型定义 + +**目标:** 创建 TypeScript 类型定义 + +**Files:** +- Create: `zenops-web/types/knowledge.ts` + +**Step 1: 创建类型定义文件** + +创建 `zenops-web/types/knowledge.ts`: + +```typescript +export interface KnowledgeDocument { + id: number; + title: string; + content: string; + doc_type: 'markdown' | 'pdf' | 'url' | 'manual'; + category: string; + tags: string[]; + enabled: boolean; + metadata: { + source_url?: string; + author?: string; + [key: string]: any; + }; + created_at: string; + updated_at: string; + score?: number; +} + +export interface CreateDocumentRequest { + title: string; + content: string; + doc_type?: string; + category: string; + tags?: string[]; + metadata?: Record; +} + +export interface UpdateDocumentRequest extends CreateDocumentRequest {} + +export interface KnowledgeStats { + total_count: number; + enabled_count: number; + categories: string[]; +} + +export interface SearchRequest { + query: string; + category?: string; +} + +export interface SearchResponse { + documents: KnowledgeDocument[]; + query: string; + total: number; +} +``` + +**Step 2: Commit** + +```bash +git add zenops-web/types/knowledge.ts +git commit -m "feat(frontend): 添加知识库 TypeScript 类型定义 + +- KnowledgeDocument: 文档模型 +- CreateDocumentRequest: 创建请求 +- KnowledgeStats: 统计信息 +- SearchRequest/Response: 搜索接口" +``` + +--- + +## Task 6: 前端 API Service + +**目标:** 创建知识库 API 调用服务 + +**Files:** +- Modify: `zenops-web/services/api.ts` + +**Step 1: 添加知识库 API** + +在 `zenops-web/services/api.ts` 文件末尾添加: + +```typescript +import { KnowledgeDocument, CreateDocumentRequest, KnowledgeStats, SearchResponse } from '../types/knowledge'; + +// 知识库 API +export const knowledgeApi = { + /** + * 获取文档列表 + */ + async listDocuments(params?: { + category?: string; + enabled?: boolean; + }): Promise { + const queryParams = new URLSearchParams(); + if (params?.category) queryParams.append('category', params.category); + if (params?.enabled !== undefined) queryParams.append('enabled', String(params.enabled)); + + const url = queryParams.toString() + ? `${API_BASE}/knowledge/documents?${queryParams}` + : `${API_BASE}/knowledge/documents`; + + const response = await fetch(url, { + headers: getAuthHeaders(), + }); + + if (!response.ok) { + throw new Error('Failed to fetch documents'); + } + + const data = await response.json(); + return data.data || []; + }, + + /** + * 获取单个文档 + */ + async getDocument(id: number): Promise { + const response = await fetch(`${API_BASE}/knowledge/documents/${id}`, { + headers: getAuthHeaders(), + }); + + if (!response.ok) { + throw new Error('Failed to fetch document'); + } + + const data = await response.json(); + return data.data; + }, + + /** + * 创建文档 + */ + async createDocument(doc: CreateDocumentRequest): Promise { + const response = await fetch(`${API_BASE}/knowledge/documents`, { + method: 'POST', + headers: getAuthHeaders(), + body: JSON.stringify(doc), + }); + + if (!response.ok) { + throw new Error('Failed to create document'); + } + + const data = await response.json(); + return data.data.id; + }, + + /** + * 更新文档 + */ + async updateDocument(id: number, doc: CreateDocumentRequest): Promise { + const response = await fetch(`${API_BASE}/knowledge/documents/${id}`, { + method: 'PUT', + headers: getAuthHeaders(), + body: JSON.stringify(doc), + }); + + if (!response.ok) { + throw new Error('Failed to update document'); + } + }, + + /** + * 删除文档 + */ + async deleteDocument(id: number): Promise { + const response = await fetch(`${API_BASE}/knowledge/documents/${id}`, { + method: 'DELETE', + headers: getAuthHeaders(), + }); + + if (!response.ok) { + throw new Error('Failed to delete document'); + } + }, + + /** + * 启用/禁用文档 + */ + async toggleDocument(id: number, enabled: boolean): Promise { + const response = await fetch(`${API_BASE}/knowledge/documents/${id}/toggle`, { + method: 'PATCH', + headers: getAuthHeaders(), + body: JSON.stringify({ enabled }), + }); + + if (!response.ok) { + throw new Error('Failed to toggle document'); + } + }, + + /** + * 获取统计信息 + */ + async getStats(): Promise { + const response = await fetch(`${API_BASE}/knowledge/stats`, { + headers: getAuthHeaders(), + }); + + if (!response.ok) { + throw new Error('Failed to fetch stats'); + } + + const data = await response.json(); + return data.data; + }, + + /** + * 搜索文档 + */ + async search(query: string, category?: string): Promise { + const response = await fetch(`${API_BASE}/knowledge/search`, { + method: 'POST', + headers: getAuthHeaders(), + body: JSON.stringify({ query, category }), + }); + + if (!response.ok) { + throw new Error('Failed to search documents'); + } + + const data = await response.json(); + return data.data; + }, +}; +``` + +**Step 2: 验证类型检查** + +Run: `cd zenops-web && npm run type-check` +Expected: 无类型错误 + +**Step 3: Commit** + +```bash +git add zenops-web/services/api.ts zenops-web/types/knowledge.ts +git commit -m "feat(frontend): 添加知识库 API Service + +- listDocuments: 获取文档列表(支持筛选) +- getDocument: 获取单个文档 +- createDocument: 创建文档 +- updateDocument: 更新文档 +- deleteDocument: 删除文档 +- toggleDocument: 启用/禁用 +- getStats: 统计信息 +- search: 搜索文档" +``` + +--- + +## Task 7: 创建组件目录结构 + +**目标:** 创建知识库组件的目录和骨架文件 + +**Files:** +- Create: `zenops-web/components/knowledge/` (目录) +- Create: `zenops-web/components/KnowledgeView.tsx` +- Create: `zenops-web/components/knowledge/DocumentList.tsx` +- Create: `zenops-web/components/knowledge/DocumentEditor.tsx` +- Create: `zenops-web/components/knowledge/CategoryTree.tsx` +- Create: `zenops-web/components/knowledge/StatsCards.tsx` + +**Step 1: 创建目录** + +Run: `mkdir -p zenops-web/components/knowledge` + +**Step 2: 创建组件骨架** + +创建每个组件文件,包含基础结构(先不实现具体逻辑): + +`zenops-web/components/KnowledgeView.tsx`: +```typescript +import React from 'react'; + +const KnowledgeView = () => { + return ( +
+

知识库管理

+ {/* TODO: 实现完整功能 */} +
+ ); +}; + +export default KnowledgeView; +``` + +`zenops-web/components/knowledge/DocumentList.tsx`: +```typescript +import React from 'react'; +import { KnowledgeDocument } from '../../types/knowledge'; + +interface Props { + documents: KnowledgeDocument[]; + loading: boolean; + onEdit: (doc: KnowledgeDocument) => void; + onDelete: () => void; + onToggle: () => void; +} + +const DocumentList: React.FC = ({ documents, loading }) => { + return
DocumentList - TODO
; +}; + +export default DocumentList; +``` + +`zenops-web/components/knowledge/DocumentEditor.tsx`: +```typescript +import React from 'react'; +import { KnowledgeDocument } from '../../types/knowledge'; + +interface Props { + document: KnowledgeDocument | null; + onClose: () => void; + onSave: () => void; +} + +const DocumentEditor: React.FC = ({ document, onClose, onSave }) => { + return
DocumentEditor - TODO
; +}; + +export default DocumentEditor; +``` + +`zenops-web/components/knowledge/CategoryTree.tsx`: +```typescript +import React from 'react'; + +interface Props { + selectedCategory: string; + onSelectCategory: (category: string) => void; +} + +const CategoryTree: React.FC = ({ selectedCategory, onSelectCategory }) => { + return
CategoryTree - TODO
; +}; + +export default CategoryTree; +``` + +`zenops-web/components/knowledge/StatsCards.tsx`: +```typescript +import React from 'react'; + +const StatsCards = () => { + return
StatsCards - TODO
; +}; + +export default StatsCards; +``` + +**Step 3: 验证编译** + +Run: `cd zenops-web && npm run type-check` +Expected: 无类型错误 + +**Step 4: Commit** + +```bash +git add zenops-web/components/KnowledgeView.tsx zenops-web/components/knowledge/ +git commit -m "feat(frontend): 创建知识库组件骨架 + +- KnowledgeView: 主容器组件 +- DocumentList: 文档列表 +- DocumentEditor: 文档编辑器 +- CategoryTree: 分类导航 +- StatsCards: 统计卡片 + +下一步实现具体逻辑" +``` + +--- + +## Task 8: 实现 KnowledgeView 主组件 + +**目标:** 实现知识库主页面逻辑和布局 + +**Files:** +- Modify: `zenops-web/components/KnowledgeView.tsx` + +**Step 1: 实现完整组件** + +替换 `KnowledgeView.tsx` 内容: + +```typescript +import React, { useState, useEffect } from 'react'; +import { Plus, Search } from 'lucide-react'; +import { knowledgeApi } from '../services/api'; +import { KnowledgeDocument } from '../types/knowledge'; +import DocumentList from './knowledge/DocumentList'; +import CategoryTree from './knowledge/CategoryTree'; +import StatsCards from './knowledge/StatsCards'; +import DocumentEditor from './knowledge/DocumentEditor'; + +const KnowledgeView = () => { + const [documents, setDocuments] = useState([]); + const [selectedCategory, setSelectedCategory] = useState(''); + const [isEditorOpen, setIsEditorOpen] = useState(false); + const [editingDoc, setEditingDoc] = useState(null); + const [loading, setLoading] = useState(true); + const [searchQuery, setSearchQuery] = useState(''); + + useEffect(() => { + loadDocuments(); + }, [selectedCategory]); + + const loadDocuments = async () => { + setLoading(true); + try { + const docs = await knowledgeApi.listDocuments({ + category: selectedCategory || undefined, + }); + setDocuments(docs); + } catch (error) { + console.error('Failed to load documents:', error); + } finally { + setLoading(false); + } + }; + + const handleCreate = () => { + setEditingDoc(null); + setIsEditorOpen(true); + }; + + const handleEdit = (doc: KnowledgeDocument) => { + setEditingDoc(doc); + setIsEditorOpen(true); + }; + + const handleSave = async () => { + setIsEditorOpen(false); + await loadDocuments(); + }; + + const handleSearch = async () => { + if (!searchQuery.trim()) { + loadDocuments(); + return; + } + + setLoading(true); + try { + const result = await knowledgeApi.search(searchQuery, selectedCategory || undefined); + setDocuments(result.documents); + } catch (error) { + console.error('Failed to search:', error); + } finally { + setLoading(false); + } + }; + + return ( +
+ {/* 统计卡片 */} + + +
+ {/* 左侧分类导航 */} +
+ +
+ + {/* 右侧文档列表 */} +
+ {/* 工具栏 */} +
+
+ + setSearchQuery(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && handleSearch()} + className="w-full pl-10 pr-4 py-2 border border-slate-200 dark:border-slate-700 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> +
+ +
+ + {/* 文档列表 */} + +
+
+ + {/* 编辑器弹窗 */} + {isEditorOpen && ( + setIsEditorOpen(false)} + onSave={handleSave} + /> + )} +
+ ); +}; + +export default KnowledgeView; +``` + +**Step 2: 验证编译** + +Run: `cd zenops-web && npm run type-check` +Expected: 无类型错误 + +**Step 3: Commit** + +```bash +git add zenops-web/components/KnowledgeView.tsx +git commit -m "feat(frontend): 实现 KnowledgeView 主组件逻辑 + +- 文档列表加载和状态管理 +- 分类筛选 +- 搜索功能 +- 创建/编辑文档弹窗控制 +- 响应式布局" +``` + +--- + +## Task 9: 实现 StatsCards 统计卡片 + +**目标:** 实现顶部统计信息展示 + +**Files:** +- Modify: `zenops-web/components/knowledge/StatsCards.tsx` + +**Step 1: 实现组件** + +```typescript +import React, { useEffect, useState } from 'react'; +import { BookOpen, CheckCircle, FolderOpen } from 'lucide-react'; +import { knowledgeApi } from '../../services/api'; +import { KnowledgeStats } from '../../types/knowledge'; + +const StatsCards = () => { + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + loadStats(); + }, []); + + const loadStats = async () => { + try { + const data = await knowledgeApi.getStats(); + setStats(data); + } catch (error) { + console.error('Failed to load stats:', error); + } finally { + setLoading(false); + } + }; + + if (loading) { + return ( +
+ {[1, 2, 3].map((i) => ( +
+
+
+
+ ))} +
+ ); + } + + return ( +
+ {/* 总文档数 */} +
+
+
+ +
+
+

总文档

+

+ {stats?.total_count || 0} +

+
+
+
+ + {/* 已启用 */} +
+
+
+ +
+
+

已启用

+

+ {stats?.enabled_count || 0} +

+
+
+
+ + {/* 分类数 */} +
+
+
+ +
+
+

分类

+

+ {stats?.categories?.length || 0} +

+
+
+
+
+ ); +}; + +export default StatsCards; +``` + +**Step 2: Commit** + +```bash +git add zenops-web/components/knowledge/StatsCards.tsx +git commit -m "feat(frontend): 实现统计卡片组件 + +- 总文档数 +- 已启用数 +- 分类数 +- 加载状态和暗黑模式支持" +``` + +--- + +## Task 10: 实现 CategoryTree 分类导航 + +**目标:** 实现左侧分类导航树 + +**Files:** +- Modify: `zenops-web/components/knowledge/CategoryTree.tsx` + +**Step 1: 实现组件** + +```typescript +import React, { useEffect, useState } from 'react'; +import { FolderOpen, BookOpen, Wrench, AlertCircle, Settings } from 'lucide-react'; +import { knowledgeApi } from '../../services/api'; + +interface Props { + selectedCategory: string; + onSelectCategory: (category: string) => void; +} + +const CategoryTree: React.FC = ({ selectedCategory, onSelectCategory }) => { + const [stats, setStats] = useState(null); + + useEffect(() => { + loadStats(); + }, []); + + const loadStats = async () => { + try { + const data = await knowledgeApi.getStats(); + setStats(data); + } catch (error) { + console.error('Failed to load stats:', error); + } + }; + + const getCategoryIcon = (category: string) => { + switch (category) { + case '运维文档': + return ; + case 'API文档': + return ; + case '故障案例': + return ; + case '配置模板': + return ; + default: + return ; + } + }; + + const getCategoryCount = (category: string) => { + // TODO: 后端返回分类统计 + return 0; + }; + + const categories = ['运维文档', 'API文档', '故障案例', '配置模板']; + + return ( +
+

分类

+ +
+ {/* 全部 */} + + + {/* 分类列表 */} + {categories.map((category) => ( + + ))} +
+
+ ); +}; + +export default CategoryTree; +``` + +**Step 2: Commit** + +```bash +git add zenops-web/components/knowledge/CategoryTree.tsx +git commit -m "feat(frontend): 实现分类导航组件 + +- 全部文档入口 +- 预定义分类(运维、API、故障、配置) +- 图标和计数显示 +- 选中状态高亮" +``` + +--- + +## Task 11: 实现 DocumentList 文档列表 + +**目标:** 实现文档列表表格和操作 + +**Files:** +- Modify: `zenops-web/components/knowledge/DocumentList.tsx` + +**Step 1: 实现组件** + +```typescript +import React from 'react'; +import { Edit, Trash2, Eye, EyeOff } from 'lucide-react'; +import { KnowledgeDocument } from '../../types/knowledge'; +import { knowledgeApi } from '../../services/api'; + +interface Props { + documents: KnowledgeDocument[]; + loading: boolean; + onEdit: (doc: KnowledgeDocument) => void; + onDelete: () => void; + onToggle: () => void; +} + +const DocumentList: React.FC = ({ documents, loading, onEdit, onDelete, onToggle }) => { + const handleToggle = async (doc: KnowledgeDocument) => { + try { + await knowledgeApi.toggleDocument(doc.id, !doc.enabled); + onToggle(); + } catch (error) { + console.error('Failed to toggle document:', error); + alert('操作失败'); + } + }; + + const handleDelete = async (doc: KnowledgeDocument) => { + if (!confirm(`确定删除文档"${doc.title}"吗?`)) { + return; + } + + try { + await knowledgeApi.deleteDocument(doc.id); + onDelete(); + } catch (error) { + console.error('Failed to delete document:', error); + alert('删除失败'); + } + }; + + if (loading) { + return ( +
+
+
+ ); + } + + if (documents.length === 0) { + return ( +
+
📚
+

+ 暂无文档 +

+

+ 点击"新建文档"添加您的第一个文档 +

+
+ ); + } + + return ( +
+ + + + + + + + + + + + + {documents.map((doc) => ( + + + + + + + + + ))} + +
+ 标题 + + 分类 + + 标签 + + 状态 + + 创建时间 + + 操作 +
+
{doc.title}
+
+ + {doc.category || '未分类'} + + +
+ {doc.tags?.slice(0, 3).map((tag, i) => ( + + #{tag} + + ))} + {doc.tags?.length > 3 && ( + +{doc.tags.length - 3} + )} +
+
+ + + {new Date(doc.created_at).toLocaleDateString('zh-CN')} + +
+ + +
+
+
+ ); +}; + +export default DocumentList; +``` + +**Step 2: Commit** + +```bash +git add zenops-web/components/knowledge/DocumentList.tsx +git commit -m "feat(frontend): 实现文档列表组件 + +- 表格展示(标题、分类、标签、状态、时间) +- 启用/禁用切换 +- 编辑和删除操作 +- 空状态提示 +- 加载状态" +``` + +--- + +## Task 12: 实现 DocumentEditor 编辑器(简化版) + +**目标:** 实现文档编辑器(先不集成 Markdown 编辑器) + +**Files:** +- Modify: `zenops-web/components/knowledge/DocumentEditor.tsx` + +**Step 1: 实现基础编辑器** + +```typescript +import React, { useState, useEffect } from 'react'; +import { X } from 'lucide-react'; +import { KnowledgeDocument, CreateDocumentRequest } from '../../types/knowledge'; +import { knowledgeApi } from '../../services/api'; + +interface Props { + document: KnowledgeDocument | null; + onClose: () => void; + onSave: () => void; +} + +const DocumentEditor: React.FC = ({ document, onClose, onSave }) => { + const [title, setTitle] = useState(''); + const [content, setContent] = useState(''); + const [category, setCategory] = useState('运维文档'); + const [tags, setTags] = useState([]); + const [tagInput, setTagInput] = useState(''); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (document) { + setTitle(document.title); + setContent(document.content); + setCategory(document.category || '运维文档'); + setTags(document.tags || []); + } + }, [document]); + + const handleAddTag = () => { + const tag = tagInput.trim(); + if (tag && !tags.includes(tag)) { + setTags([...tags, tag]); + setTagInput(''); + } + }; + + const handleRemoveTag = (tagToRemove: string) => { + setTags(tags.filter((t) => t !== tagToRemove)); + }; + + const handleSave = async () => { + if (!title.trim() || !content.trim()) { + alert('标题和内容不能为空'); + return; + } + + setSaving(true); + try { + const req: CreateDocumentRequest = { + title, + content, + category, + tags, + doc_type: 'markdown', + }; + + if (document) { + await knowledgeApi.updateDocument(document.id, req); + } else { + await knowledgeApi.createDocument(req); + } + + onSave(); + } catch (error) { + console.error('Failed to save document:', error); + alert('保存失败'); + } finally { + setSaving(false); + } + }; + + return ( +
+
+ {/* Header */} +
+

+ {document ? '编辑文档' : '新建文档'} +

+
+ + +
+
+ + {/* Body */} +
+ {/* 标题 */} +
+ + setTitle(e.target.value)} + placeholder="请输入文档标题" + className="w-full px-4 py-2 border border-slate-200 dark:border-slate-700 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> +
+ + {/* 分类 */} +
+ + +
+ + {/* 标签 */} +
+ +
+ setTagInput(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && handleAddTag()} + placeholder="输入标签,按回车添加" + className="flex-1 px-4 py-2 border border-slate-200 dark:border-slate-700 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500" + /> + +
+
+ {tags.map((tag) => ( + + #{tag} + + + ))} +
+
+ + {/* 内容 (简化版 Textarea) */} +
+ +