From bd93c12edcb89a7ed3a368243a5eae736421a3e4 Mon Sep 17 00:00:00 2001 From: lpf Date: Tue, 3 Mar 2026 10:36:53 +0800 Subject: [PATCH] fix logs --- cmd/clawgo/cmd_agent.go | 2 +- cmd/clawgo/cmd_gateway.go | 12 +- pkg/agent/context.go | 4 +- pkg/agent/loop.go | 45 +- pkg/bus/bus.go | 8 +- pkg/channels/base.go | 10 +- pkg/channels/dingtalk.go | 12 +- pkg/channels/discord.go | 20 +- pkg/channels/feishu.go | 20 +- pkg/channels/maixcam.go | 34 +- pkg/channels/manager.go | 80 ++-- pkg/channels/qq.go | 22 +- pkg/channels/telegram.go | 59 ++- pkg/channels/utils.go | 8 +- pkg/channels/whatsapp.go | 16 +- pkg/logger/codes.go | 179 ++++++++ pkg/logger/logger.go | 127 +++--- pkg/providers/http_provider.go | 4 +- pkg/sentinel/service.go | 42 +- pkg/server/server.go | 8 +- pkg/tools/registry.go | 8 +- webui/public/log-codes.json | 698 +++++++++++++++++++++++++++++++ webui/src/App.tsx | 2 + webui/src/components/Sidebar.tsx | 3 +- webui/src/i18n/index.ts | 2 + webui/src/pages/Chat.tsx | 2 +- webui/src/pages/Cron.tsx | 2 +- webui/src/pages/LogCodes.tsx | 81 ++++ webui/src/pages/Logs.tsx | 61 ++- webui/src/types/index.ts | 2 + 30 files changed, 1311 insertions(+), 262 deletions(-) create mode 100644 pkg/logger/codes.go create mode 100644 webui/public/log-codes.json create mode 100644 webui/src/pages/LogCodes.tsx diff --git a/cmd/clawgo/cmd_agent.go b/cmd/clawgo/cmd_agent.go index 0c1c26c..5920a97 100644 --- a/cmd/clawgo/cmd_agent.go +++ b/cmd/clawgo/cmd_agent.go @@ -62,7 +62,7 @@ func agentCmd() { agentLoop := agent.NewAgentLoop(cfg, msgBus, provider, cronService) startupInfo := agentLoop.GetStartupInfo() - logger.InfoCF("agent", "Agent initialized", + logger.InfoCF("agent", logger.C0098, map[string]interface{}{ "tools_count": startupInfo["tools"].(map[string]interface{})["count"], "skills_total": startupInfo["skills"].(map[string]interface{})["total"], diff --git a/cmd/clawgo/cmd_gateway.go b/cmd/clawgo/cmd_gateway.go index f316d4f..5381266 100644 --- a/cmd/clawgo/cmd_gateway.go +++ b/cmd/clawgo/cmd_gateway.go @@ -614,7 +614,7 @@ func runGatewayStartupCompactionCheck(parent context.Context, agentLoop *agent.A defer cancel() report := agentLoop.RunStartupSelfCheckAllSessions(checkCtx) - logger.InfoCF("gateway", "Startup compaction check completed", map[string]interface{}{ + logger.InfoCF("gateway", logger.C0110, map[string]interface{}{ "sessions_total": report.TotalSessions, "sessions_compacted": report.CompactedSessions, }) @@ -641,20 +641,20 @@ func runGatewayBootstrapInit(parent context.Context, cfg *config.Config, agentLo prompt := "System startup bootstrap: read BOOTSTRAP.md and perform one-time self-initialization checks now. If already initialized, return concise status only." resp, err := agentLoop.ProcessDirect(initCtx, prompt, "system:bootstrap:init") if err != nil { - logger.ErrorCF("gateway", "Bootstrap init model call failed", map[string]interface{}{logger.FieldError: err.Error()}) + logger.ErrorCF("gateway", logger.C0111, map[string]interface{}{logger.FieldError: err.Error()}) return } line := fmt.Sprintf("%s\n%s\n", time.Now().UTC().Format(time.RFC3339), strings.TrimSpace(resp)) if err := os.WriteFile(markerPath, []byte(line), 0644); err != nil { - logger.ErrorCF("gateway", "Bootstrap init marker write failed", map[string]interface{}{logger.FieldError: err.Error()}) + logger.ErrorCF("gateway", logger.C0112, map[string]interface{}{logger.FieldError: err.Error()}) return } // Bootstrap only runs once. After successful initialization marker is written, // remove BOOTSTRAP.md to avoid repeated first-run guidance. if err := os.Remove(bootstrapPath); err != nil && !os.IsNotExist(err) { - logger.WarnCF("gateway", "Bootstrap file cleanup failed", map[string]interface{}{logger.FieldError: err.Error()}) + logger.WarnCF("gateway", logger.C0113, map[string]interface{}{logger.FieldError: err.Error()}) } - logger.InfoC("gateway", "Bootstrap init model call completed") + logger.InfoC("gateway", logger.C0114) } func applyMaximumPermissionPolicy(cfg *config.Config) { @@ -854,7 +854,7 @@ func buildGatewayRuntime(ctx context.Context, cfg *config.Config, msgBus *bus.Me fmt.Printf(" • Tools: %d loaded\n", toolsInfo["count"]) fmt.Printf(" • Skills: %d/%d available\n", skillsInfo["available"], skillsInfo["total"]) - logger.InfoCF("agent", "Agent initialized", + logger.InfoCF("agent", logger.C0098, map[string]interface{}{ "tools_count": toolsInfo["count"], "skills_total": skillsInfo["total"], diff --git a/pkg/agent/context.go b/pkg/agent/context.go index 4fa4975..0b6f0b9 100644 --- a/pkg/agent/context.go +++ b/pkg/agent/context.go @@ -196,7 +196,7 @@ func (cb *ContextBuilder) BuildMessages(history []providers.Message, summary str } // Log system prompt summary for debugging (debug mode only) - logger.DebugCF("agent", "System prompt built", + logger.DebugCF("agent", logger.C0143, map[string]interface{}{ "total_chars": len(systemPrompt), "total_lines": strings.Count(systemPrompt, "\n") + 1, @@ -208,7 +208,7 @@ func (cb *ContextBuilder) BuildMessages(history []providers.Message, summary str if len(preview) > 500 { preview = preview[:500] + "... (truncated)" } - logger.DebugCF("agent", "System prompt preview", + logger.DebugCF("agent", logger.C0144, map[string]interface{}{ "preview": preview, }) diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 5df8692..6bd6fe4 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -326,7 +326,7 @@ func (al *AgentLoop) buildSessionShards(ctx context.Context) []chan bus.InboundM } }(shards[i]) } - logger.InfoCF("agent", "Session-sharded dispatcher enabled", map[string]interface{}{"shards": count}) + logger.InfoCF("agent", logger.C0149, map[string]interface{}{"shards": count}) return shards } @@ -377,7 +377,7 @@ func (al *AgentLoop) tryFallbackProviders(ctx context.Context, msg bus.InboundMe al.ekg.Record(ekg.Event{Session: msg.SessionKey, Channel: msg.Channel, Source: "provider_fallback", Status: st, Provider: name, Model: al.model, ErrSig: errSig, Log: lg}) } if err == nil { - logger.WarnCF("agent", "LLM fallback provider switched", map[string]interface{}{"provider": name}) + logger.WarnCF("agent", logger.C0150, map[string]interface{}{"provider": name}) return resp, name, nil } lastErr = err @@ -655,12 +655,13 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) } // Add message preview to log preview := truncate(msg.Content, 80) - logger.InfoCF("agent", fmt.Sprintf("Processing message from %s:%s: %s", msg.Channel, msg.SenderID, preview), + logger.InfoCF("agent", logger.C0171, map[string]interface{}{ "channel": msg.Channel, "chat_id": msg.ChatID, "sender_id": msg.SenderID, "session_key": msg.SessionKey, + "preview": preview, }) // Route system messages to processSystemMessage @@ -769,7 +770,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) for iteration < maxAllowed { iteration++ - logger.DebugCF("agent", "LLM iteration", + logger.DebugCF("agent", logger.C0151, map[string]interface{}{ "iteration": iteration, "max": al.maxIterations, @@ -789,7 +790,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) } // Log LLM request details - logger.DebugCF("agent", "LLM request", + logger.DebugCF("agent", logger.C0152, map[string]interface{}{ "iteration": iteration, "model": al.model, @@ -801,7 +802,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) }) // Log full messages (detailed) - logger.DebugCF("agent", "Full LLM request", + logger.DebugCF("agent", logger.C0153, map[string]interface{}{ "iteration": iteration, "messages_json": formatMessagesForLog(messages), @@ -852,9 +853,9 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) errText := strings.ToLower(err.Error()) if strings.Contains(errText, "no tool call found for function call output") { removed := al.sessions.PurgeOrphanToolOutputs(msg.SessionKey) - logger.WarnCF("agent", "Purged orphan tool outputs after provider pairing error", map[string]interface{}{"session_key": msg.SessionKey, "removed": removed}) + logger.WarnCF("agent", logger.C0154, map[string]interface{}{"session_key": msg.SessionKey, "removed": removed}) } - logger.ErrorCF("agent", "LLM call failed", + logger.ErrorCF("agent", logger.C0155, map[string]interface{}{ "iteration": iteration, "error": err.Error(), @@ -864,7 +865,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) if len(response.ToolCalls) == 0 { finalContent = response.Content - logger.InfoCF("agent", "LLM response without tool calls (direct answer)", + logger.InfoCF("agent", logger.C0156, map[string]interface{}{ "iteration": iteration, "content_chars": len(finalContent), @@ -876,7 +877,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) for _, tc := range response.ToolCalls { toolNames = append(toolNames, tc.Name) } - logger.InfoCF("agent", "LLM requested tool calls", + logger.InfoCF("agent", logger.C0157, map[string]interface{}{ "tools": toolNames, "count": len(toolNames), @@ -918,9 +919,10 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) // Log tool call with arguments preview argsJSON, _ := json.Marshal(tc.Arguments) argsPreview := truncate(string(argsJSON), 200) - logger.InfoCF("agent", fmt.Sprintf("Tool call: %s(%s)", tc.Name, argsPreview), + logger.InfoCF("agent", logger.C0172, map[string]interface{}{ "tool": tc.Name, + "args": argsPreview, "iteration": iteration, }) @@ -996,8 +998,11 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) // Log response preview (original content) responsePreview := truncate(finalContent, 120) - logger.InfoCF("agent", fmt.Sprintf("Response to %s:%s: %s", msg.Channel, msg.SenderID, responsePreview), + logger.InfoCF("agent", logger.C0173, map[string]interface{}{ + "channel": msg.Channel, + "sender_id": msg.SenderID, + "preview": responsePreview, "iterations": iteration, "final_length": len(finalContent), "user_length": len(userContent), @@ -1030,7 +1035,7 @@ func (al *AgentLoop) appendDailySummaryLog(msg bus.InboundMessage, response stri truncate(strings.ReplaceAll(respText, "\n", " "), 220), ) if err := ms.AppendToday(line); err != nil { - logger.WarnCF("agent", "append daily summary log failed", map[string]interface{}{logger.FieldError: err.Error()}) + logger.WarnCF("agent", logger.C0158, map[string]interface{}{logger.FieldError: err.Error()}) } } @@ -1089,7 +1094,7 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe return "", fmt.Errorf("processSystemMessage called with non-system message channel: %s", msg.Channel) } - logger.InfoCF("agent", "Processing system message", + logger.InfoCF("agent", logger.C0159, map[string]interface{}{ "sender_id": msg.SenderID, "chat_id": msg.ChatID, @@ -1158,7 +1163,7 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe } // Log LLM request details - logger.DebugCF("agent", "LLM request", + logger.DebugCF("agent", logger.C0152, map[string]interface{}{ "iteration": iteration, "model": al.model, @@ -1170,7 +1175,7 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe }) // Log full messages (detailed) - logger.DebugCF("agent", "Full LLM request", + logger.DebugCF("agent", logger.C0153, map[string]interface{}{ "iteration": iteration, "messages_json": formatMessagesForLog(messages), @@ -1198,7 +1203,7 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe errText := strings.ToLower(err.Error()) if strings.Contains(errText, "no tool call found for function call output") { removed := al.sessions.PurgeOrphanToolOutputs(sessionKey) - logger.WarnCF("agent", "Purged orphan tool outputs after provider pairing error (system)", map[string]interface{}{"session_key": sessionKey, "removed": removed}) + logger.WarnCF("agent", logger.C0160, map[string]interface{}{"session_key": sessionKey, "removed": removed}) if removed > 0 { // Rebuild context from cleaned history and retry current iteration. history = al.sessions.GetHistory(sessionKey) @@ -1225,11 +1230,11 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe originChatID, responseLang, ) - logger.WarnCF("agent", "Heartbeat session reset after repeated provider pairing error", map[string]interface{}{"session_key": sessionKey}) + logger.WarnCF("agent", logger.C0161, map[string]interface{}{"session_key": sessionKey}) continue } } - logger.ErrorCF("agent", "LLM call failed in system message", + logger.ErrorCF("agent", logger.C0162, map[string]interface{}{ "iteration": iteration, "error": err.Error(), @@ -1297,7 +1302,7 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe al.sessions.Save(al.sessions.GetOrCreate(sessionKey)) - logger.InfoCF("agent", "System message processing completed", + logger.InfoCF("agent", logger.C0163, map[string]interface{}{ "iterations": iteration, "final_length": len(finalContent), diff --git a/pkg/bus/bus.go b/pkg/bus/bus.go index f9d9136..c458def 100644 --- a/pkg/bus/bus.go +++ b/pkg/bus/bus.go @@ -37,7 +37,7 @@ func (mb *MessageBus) PublishInbound(msg InboundMessage) { defer func() { if recover() != nil { - logger.WarnCF("bus", "PublishInbound on closed channel recovered", map[string]interface{}{ + logger.WarnCF("bus", logger.C0129, map[string]interface{}{ logger.FieldChannel: msg.Channel, logger.FieldChatID: msg.ChatID, "session_key": msg.SessionKey, @@ -48,7 +48,7 @@ func (mb *MessageBus) PublishInbound(msg InboundMessage) { select { case ch <- msg: case <-time.After(queueWriteTimeout): - logger.ErrorCF("bus", "PublishInbound timeout (queue full)", map[string]interface{}{ + logger.ErrorCF("bus", logger.C0130, map[string]interface{}{ logger.FieldChannel: msg.Channel, logger.FieldChatID: msg.ChatID, "session_key": msg.SessionKey, @@ -76,7 +76,7 @@ func (mb *MessageBus) PublishOutbound(msg OutboundMessage) { defer func() { if recover() != nil { - logger.WarnCF("bus", "PublishOutbound on closed channel recovered", map[string]interface{}{ + logger.WarnCF("bus", logger.C0131, map[string]interface{}{ logger.FieldChannel: msg.Channel, logger.FieldChatID: msg.ChatID, }) @@ -86,7 +86,7 @@ func (mb *MessageBus) PublishOutbound(msg OutboundMessage) { select { case ch <- msg: case <-time.After(queueWriteTimeout): - logger.ErrorCF("bus", "PublishOutbound timeout (queue full)", map[string]interface{}{ + logger.ErrorCF("bus", logger.C0132, map[string]interface{}{ logger.FieldChannel: msg.Channel, logger.FieldChatID: msg.ChatID, }) diff --git a/pkg/channels/base.go b/pkg/channels/base.go index 3f28e99..7c6ad7d 100644 --- a/pkg/channels/base.go +++ b/pkg/channels/base.go @@ -129,7 +129,7 @@ func messageDigest(s string) string { func (c *BaseChannel) HandleMessage(senderID, chatID, content string, media []string, metadata map[string]string) { if !c.IsAllowed(senderID) { - logger.WarnCF("channels", "Message rejected by allowlist", map[string]interface{}{ + logger.WarnCF("channels", logger.C0001, map[string]interface{}{ logger.FieldChannel: c.name, logger.FieldSenderID: senderID, logger.FieldChatID: chatID, @@ -140,10 +140,10 @@ func (c *BaseChannel) HandleMessage(senderID, chatID, content string, media []st if metadata != nil { if messageID := strings.TrimSpace(metadata["message_id"]); messageID != "" { if c.seenRecently(c.name+":"+messageID, inboundMessageIDDedupeTTL) { - logger.WarnCF("channels", "Duplicate inbound message skipped", map[string]interface{}{ + logger.WarnCF("channels", logger.C0002, map[string]interface{}{ logger.FieldChannel: c.name, - "message_id": messageID, - logger.FieldChatID: chatID, + "message_id": messageID, + logger.FieldChatID: chatID, }) return } @@ -152,7 +152,7 @@ func (c *BaseChannel) HandleMessage(senderID, chatID, content string, media []st // Fallback dedupe when platform omits/changes message_id (short window, same sender/chat/content). contentKey := c.name + ":content:" + chatID + ":" + senderID + ":" + messageDigest(content) if c.seenRecently(contentKey, inboundContentDedupeTTL) { - logger.WarnCF("channels", "Duplicate inbound content skipped", map[string]interface{}{ + logger.WarnCF("channels", logger.C0003, map[string]interface{}{ logger.FieldChannel: c.name, logger.FieldChatID: chatID, }) diff --git a/pkg/channels/dingtalk.go b/pkg/channels/dingtalk.go index 240b8f0..e1b408d 100644 --- a/pkg/channels/dingtalk.go +++ b/pkg/channels/dingtalk.go @@ -49,7 +49,7 @@ func (c *DingTalkChannel) Start(ctx context.Context) error { if c.IsRunning() { return nil } - logger.InfoC("dingtalk", "Starting DingTalk channel (Stream Mode)") + logger.InfoC("dingtalk", logger.C0115) runCtx, cancel := context.WithCancel(ctx) c.runCancel.set(cancel) @@ -72,7 +72,7 @@ func (c *DingTalkChannel) Start(ctx context.Context) error { } c.setRunning(true) - logger.InfoC("dingtalk", "DingTalk channel started (Stream Mode)") + logger.InfoC("dingtalk", logger.C0116) return nil } @@ -81,7 +81,7 @@ func (c *DingTalkChannel) Stop(ctx context.Context) error { if !c.IsRunning() { return nil } - logger.InfoC("dingtalk", "Stopping DingTalk channel") + logger.InfoC("dingtalk", logger.C0117) c.runCancel.cancelAndClear() @@ -90,7 +90,7 @@ func (c *DingTalkChannel) Stop(ctx context.Context) error { } c.setRunning(false) - logger.InfoC("dingtalk", "DingTalk channel stopped") + logger.InfoC("dingtalk", logger.C0118) return nil } @@ -111,7 +111,7 @@ func (c *DingTalkChannel) Send(ctx context.Context, msg bus.OutboundMessage) err return fmt.Errorf("invalid session_webhook type for chat %s", msg.ChatID) } - logger.InfoCF("dingtalk", "DingTalk outbound message", map[string]interface{}{ + logger.InfoCF("dingtalk", logger.C0119, map[string]interface{}{ logger.FieldChatID: msg.ChatID, logger.FieldPreview: truncateString(msg.Content, 100), "platform": "dingtalk", @@ -159,7 +159,7 @@ func (c *DingTalkChannel) onChatBotMessageReceived(ctx context.Context, data *ch "session_webhook": data.SessionWebhook, } - logger.InfoCF("dingtalk", "DingTalk inbound message", map[string]interface{}{ + logger.InfoCF("dingtalk", logger.C0120, map[string]interface{}{ "sender_name": senderNick, logger.FieldSenderID: senderID, logger.FieldChatID: chatID, diff --git a/pkg/channels/discord.go b/pkg/channels/discord.go index 94a1aba..f820120 100644 --- a/pkg/channels/discord.go +++ b/pkg/channels/discord.go @@ -37,7 +37,7 @@ func NewDiscordChannel(cfg config.DiscordConfig, bus *bus.MessageBus) (*DiscordC } func (c *DiscordChannel) Start(ctx context.Context) error { - logger.InfoC("discord", "Starting Discord bot") + logger.InfoC("discord", logger.C0069) c.session.AddHandler(c.handleMessage) @@ -51,7 +51,7 @@ func (c *DiscordChannel) Start(ctx context.Context) error { if err != nil { return fmt.Errorf("failed to get bot user: %w", err) } - logger.InfoCF("discord", "Discord bot connected", map[string]interface{}{ + logger.InfoCF("discord", logger.C0070, map[string]interface{}{ "username": botUser.Username, "user_id": botUser.ID, }) @@ -60,7 +60,7 @@ func (c *DiscordChannel) Start(ctx context.Context) error { } func (c *DiscordChannel) Stop(ctx context.Context) error { - logger.InfoC("discord", "Stopping Discord bot") + logger.InfoC("discord", logger.C0071) c.setRunning(false) if err := c.session.Close(); err != nil { @@ -145,7 +145,7 @@ func (c *DiscordChannel) handleMessage(s *discordgo.Session, m *discordgo.Messag content = "[media only]" } - logger.DebugCF("discord", "Received message", map[string]interface{}{ + logger.DebugCF("discord", logger.C0072, map[string]interface{}{ "sender_name": senderName, logger.FieldSenderID: senderID, logger.FieldPreview: truncateString(content, 50), @@ -186,7 +186,7 @@ func isAudioFile(filename, contentType string) bool { func (c *DiscordChannel) downloadAttachment(url, filename string) string { mediaDir := filepath.Join(os.TempDir(), "clawgo_media") if err := os.MkdirAll(mediaDir, 0755); err != nil { - logger.WarnCF("discord", "Failed to create media directory", map[string]interface{}{ + logger.WarnCF("discord", logger.C0073, map[string]interface{}{ logger.FieldError: err.Error(), }) return "" @@ -196,7 +196,7 @@ func (c *DiscordChannel) downloadAttachment(url, filename string) string { resp, err := http.Get(url) if err != nil { - logger.WarnCF("discord", "Failed to download attachment", map[string]interface{}{ + logger.WarnCF("discord", logger.C0074, map[string]interface{}{ logger.FieldError: err.Error(), }) return "" @@ -204,7 +204,7 @@ func (c *DiscordChannel) downloadAttachment(url, filename string) string { defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - logger.WarnCF("discord", "Attachment download returned non-200", map[string]interface{}{ + logger.WarnCF("discord", logger.C0075, map[string]interface{}{ "status_code": resp.StatusCode, }) return "" @@ -212,7 +212,7 @@ func (c *DiscordChannel) downloadAttachment(url, filename string) string { out, err := os.Create(localPath) if err != nil { - logger.WarnCF("discord", "Failed to create local attachment file", map[string]interface{}{ + logger.WarnCF("discord", logger.C0076, map[string]interface{}{ logger.FieldError: err.Error(), }) return "" @@ -221,13 +221,13 @@ func (c *DiscordChannel) downloadAttachment(url, filename string) string { _, err = io.Copy(out, resp.Body) if err != nil { - logger.WarnCF("discord", "Failed to write local attachment file", map[string]interface{}{ + logger.WarnCF("discord", logger.C0077, map[string]interface{}{ logger.FieldError: err.Error(), }) return "" } - logger.DebugCF("discord", "Attachment downloaded successfully", map[string]interface{}{ + logger.DebugCF("discord", logger.C0078, map[string]interface{}{ "path": localPath, }) return localPath diff --git a/pkg/channels/feishu.go b/pkg/channels/feishu.go index 1c643b2..d9f8af0 100644 --- a/pkg/channels/feishu.go +++ b/pkg/channels/feishu.go @@ -86,7 +86,7 @@ func (c *FeishuChannel) Start(ctx context.Context) error { c.mu.Unlock() c.setRunning(true) - logger.InfoC("feishu", "Feishu channel started (websocket mode)") + logger.InfoC("feishu", logger.C0043) runChannelTask("feishu", "websocket", func() error { return wsClient.Start(runCtx) @@ -107,7 +107,7 @@ func (c *FeishuChannel) Stop(ctx context.Context) error { c.runCancel.cancelAndClear() c.setRunning(false) - logger.InfoC("feishu", "Feishu channel stopped") + logger.InfoC("feishu", logger.C0044) return nil } @@ -132,7 +132,7 @@ func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMessage) error for i, t := range tables { link, lerr := c.createFeishuSheetFromTable(ctx, t.Name, t.Rows) if lerr != nil { - logger.WarnCF("feishu", "create sheet from markdown table failed", map[string]interface{}{logger.FieldError: lerr.Error(), logger.FieldChatID: msg.ChatID}) + logger.WarnCF("feishu", logger.C0045, map[string]interface{}{logger.FieldError: lerr.Error(), logger.FieldChatID: msg.ChatID}) continue } links = append(links, fmt.Sprintf("表格%d: %s", i+1, link)) @@ -160,7 +160,7 @@ func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMessage) error return err } - logger.InfoCF("feishu", "Feishu message sent", map[string]interface{}{ + logger.InfoCF("feishu", logger.C0046, map[string]interface{}{ logger.FieldChatID: msg.ChatID, "msg_type": msgType, "has_media": strings.TrimSpace(workMsg.Media) != "", @@ -184,7 +184,7 @@ func (c *FeishuChannel) handleMessageReceive(ctx context.Context, event *larkim. } chatType := strings.ToLower(strings.TrimSpace(stringValue(message.ChatType))) if !c.isAllowedChat(chatID, chatType) { - logger.WarnCF("feishu", "Feishu message rejected by chat allowlist", map[string]interface{}{ + logger.WarnCF("feishu", logger.C0047, map[string]interface{}{ logger.FieldSenderID: extractFeishuSenderID(sender), logger.FieldChatID: chatID, "chat_type": chatType, @@ -202,7 +202,7 @@ func (c *FeishuChannel) handleMessageReceive(ctx context.Context, event *larkim. content = "[empty message]" } if !c.shouldHandleGroupMessage(chatType, content) { - logger.DebugCF("feishu", "Ignoring group message without mention/command", map[string]interface{}{ + logger.DebugCF("feishu", logger.C0048, map[string]interface{}{ logger.FieldSenderID: senderID, logger.FieldChatID: chatID, }) @@ -223,7 +223,7 @@ func (c *FeishuChannel) handleMessageReceive(ctx context.Context, event *larkim. metadata["tenant_key"] = *sender.TenantKey } - logger.InfoCF("feishu", "Feishu message received", map[string]interface{}{ + logger.InfoCF("feishu", logger.C0049, map[string]interface{}{ logger.FieldSenderID: senderID, logger.FieldChatID: chatID, logger.FieldPreview: truncateString(content, 80), @@ -246,7 +246,7 @@ func (c *FeishuChannel) resolveInboundMedia(ctx context.Context, mediaRefs []str if path, err := c.downloadFeishuMediaByKey(ctx, "image", key); err == nil { out = append(out, path) } else { - logger.WarnCF("feishu", "download inbound image failed", map[string]interface{}{logger.FieldError: err.Error(), "image_key": key}) + logger.WarnCF("feishu", logger.C0050, map[string]interface{}{logger.FieldError: err.Error(), "image_key": key}) out = append(out, ref) } continue @@ -256,7 +256,7 @@ func (c *FeishuChannel) resolveInboundMedia(ctx context.Context, mediaRefs []str if path, err := c.downloadFeishuMediaByKey(ctx, "file", key); err == nil { out = append(out, path) } else { - logger.WarnCF("feishu", "download inbound file failed", map[string]interface{}{logger.FieldError: err.Error(), "file_key": key}) + logger.WarnCF("feishu", logger.C0051, map[string]interface{}{logger.FieldError: err.Error(), "file_key": key}) out = append(out, ref) } continue @@ -674,7 +674,7 @@ func (c *FeishuChannel) createFeishuSheetFromTable(ctx context.Context, name str } } if err := c.setFeishuSheetPublicEditable(ctx, spToken); err != nil { - logger.WarnCF("feishu", "set sheet permission failed", map[string]interface{}{logger.FieldError: err.Error(), "sheet_token": spToken}) + logger.WarnCF("feishu", logger.C0052, map[string]interface{}{logger.FieldError: err.Error(), "sheet_token": spToken}) } if createResp.Data.Spreadsheet.Url != nil && strings.TrimSpace(*createResp.Data.Spreadsheet.Url) != "" { return strings.TrimSpace(*createResp.Data.Spreadsheet.Url), nil diff --git a/pkg/channels/maixcam.go b/pkg/channels/maixcam.go index 7c3cb9a..e97e713 100644 --- a/pkg/channels/maixcam.go +++ b/pkg/channels/maixcam.go @@ -38,7 +38,7 @@ func NewMaixCamChannel(cfg config.MaixCamConfig, bus *bus.MessageBus) (*MaixCamC } func (c *MaixCamChannel) Start(ctx context.Context) error { - logger.InfoC("maixcam", "Starting MaixCam channel server") + logger.InfoC("maixcam", logger.C0079) addr := fmt.Sprintf("%s:%d", c.config.Host, c.config.Port) listener, err := net.Listen("tcp", addr) @@ -49,7 +49,7 @@ func (c *MaixCamChannel) Start(ctx context.Context) error { c.listener = listener c.setRunning(true) - logger.InfoCF("maixcam", "MaixCam server listening", map[string]interface{}{ + logger.InfoCF("maixcam", logger.C0080, map[string]interface{}{ "host": c.config.Host, "port": c.config.Port, }) @@ -60,25 +60,25 @@ func (c *MaixCamChannel) Start(ctx context.Context) error { } func (c *MaixCamChannel) acceptConnections(ctx context.Context) { - logger.DebugC("maixcam", "Starting connection acceptor") + logger.DebugC("maixcam", logger.C0081) for { select { case <-ctx.Done(): - logger.InfoC("maixcam", "Stopping connection acceptor") + logger.InfoC("maixcam", logger.C0082) return default: conn, err := c.listener.Accept() if err != nil { if c.IsRunning() { - logger.ErrorCF("maixcam", "Failed to accept connection", map[string]interface{}{ + logger.ErrorCF("maixcam", logger.C0083, map[string]interface{}{ logger.FieldError: err.Error(), }) } return } - logger.InfoCF("maixcam", "New connection from MaixCam device", map[string]interface{}{ + logger.InfoCF("maixcam", logger.C0084, map[string]interface{}{ "remote_addr": conn.RemoteAddr().String(), }) @@ -92,14 +92,14 @@ func (c *MaixCamChannel) acceptConnections(ctx context.Context) { } func (c *MaixCamChannel) handleConnection(conn net.Conn, ctx context.Context) { - logger.DebugC("maixcam", "Handling MaixCam connection") + logger.DebugC("maixcam", logger.C0085) defer func() { conn.Close() c.clientsMux.Lock() delete(c.clients, conn) c.clientsMux.Unlock() - logger.DebugC("maixcam", "Connection closed") + logger.DebugC("maixcam", logger.C0086) }() decoder := json.NewDecoder(conn) @@ -112,7 +112,7 @@ func (c *MaixCamChannel) handleConnection(conn net.Conn, ctx context.Context) { var msg MaixCamMessage if err := decoder.Decode(&msg); err != nil { if err.Error() != "EOF" { - logger.ErrorCF("maixcam", "Failed to decode message", map[string]interface{}{ + logger.ErrorCF("maixcam", logger.C0087, map[string]interface{}{ logger.FieldError: err.Error(), }) } @@ -129,18 +129,18 @@ func (c *MaixCamChannel) processMessage(msg MaixCamMessage, conn net.Conn) { case "person_detected": c.handlePersonDetection(msg) case "heartbeat": - logger.DebugC("maixcam", "Received heartbeat") + logger.DebugC("maixcam", logger.C0088) case "status": c.handleStatusUpdate(msg) default: - logger.WarnCF("maixcam", "Unknown message type", map[string]interface{}{ + logger.WarnCF("maixcam", logger.C0089, map[string]interface{}{ "message_type": msg.Type, }) } } func (c *MaixCamChannel) handlePersonDetection(msg MaixCamMessage) { - logger.InfoCF("maixcam", "Person detected event", map[string]interface{}{ + logger.InfoCF("maixcam", logger.C0090, map[string]interface{}{ logger.FieldSenderID: "maixcam", logger.FieldChatID: "default", "timestamp": msg.Timestamp, @@ -178,13 +178,13 @@ func (c *MaixCamChannel) handlePersonDetection(msg MaixCamMessage) { } func (c *MaixCamChannel) handleStatusUpdate(msg MaixCamMessage) { - logger.InfoCF("maixcam", "Status update from MaixCam", map[string]interface{}{ + logger.InfoCF("maixcam", logger.C0091, map[string]interface{}{ "status": msg.Data, }) } func (c *MaixCamChannel) Stop(ctx context.Context) error { - logger.InfoC("maixcam", "Stopping MaixCam channel") + logger.InfoC("maixcam", logger.C0092) c.setRunning(false) if c.listener != nil { @@ -199,7 +199,7 @@ func (c *MaixCamChannel) Stop(ctx context.Context) error { } c.clients = make(map[net.Conn]bool) - logger.InfoC("maixcam", "MaixCam channel stopped") + logger.InfoC("maixcam", logger.C0093) return nil } @@ -212,7 +212,7 @@ func (c *MaixCamChannel) Send(ctx context.Context, msg bus.OutboundMessage) erro defer c.clientsMux.RUnlock() if len(c.clients) == 0 { - logger.WarnC("maixcam", "No MaixCam devices connected") + logger.WarnC("maixcam", logger.C0094) return fmt.Errorf("no connected MaixCam devices") } @@ -231,7 +231,7 @@ func (c *MaixCamChannel) Send(ctx context.Context, msg bus.OutboundMessage) erro var sendErr error for conn := range c.clients { if _, err := conn.Write(data); err != nil { - logger.ErrorCF("maixcam", "Failed to send to client", map[string]interface{}{ + logger.ErrorCF("maixcam", logger.C0095, map[string]interface{}{ "client": conn.RemoteAddr().String(), logger.FieldError: err.Error(), }) diff --git a/pkg/channels/manager.go b/pkg/channels/manager.go index 562b0ab..3670b0f 100644 --- a/pkg/channels/manager.go +++ b/pkg/channels/manager.go @@ -71,39 +71,39 @@ func NewManager(cfg *config.Config, messageBus *bus.MessageBus) (*Manager, error } func (m *Manager) initChannels() error { - logger.InfoC("channels", "Initializing channel manager") + logger.InfoC("channels", logger.C0004) if m.config.Channels.Telegram.Enabled { - logger.DebugCF("channels", "Attempting to initialize Telegram channel", map[string]interface{}{ + logger.DebugCF("channels", logger.C0005, map[string]interface{}{ "has_token": m.config.Channels.Telegram.Token != "", }) if m.config.Channels.Telegram.Token == "" { - logger.WarnC("channels", "Telegram token is empty, skipping") + logger.WarnC("channels", logger.C0006) } else { telegram, err := NewTelegramChannel(m.config.Channels.Telegram, m.bus) if err != nil { - logger.ErrorCF("channels", "Failed to initialize Telegram channel", map[string]interface{}{ + logger.ErrorCF("channels", logger.C0007, map[string]interface{}{ logger.FieldError: err.Error(), }) } else { m.channels["telegram"] = telegram - logger.InfoC("channels", "Telegram channel enabled successfully") + logger.InfoC("channels", logger.C0008) } } } if m.config.Channels.WhatsApp.Enabled { if m.config.Channels.WhatsApp.BridgeURL == "" { - logger.WarnC("channels", "WhatsApp bridge URL is empty, skipping") + logger.WarnC("channels", logger.C0009) } else { whatsapp, err := NewWhatsAppChannel(m.config.Channels.WhatsApp, m.bus) if err != nil { - logger.ErrorCF("channels", "Failed to initialize WhatsApp channel", map[string]interface{}{ + logger.ErrorCF("channels", logger.C0010, map[string]interface{}{ logger.FieldError: err.Error(), }) } else { m.channels["whatsapp"] = whatsapp - logger.InfoC("channels", "WhatsApp channel enabled successfully") + logger.InfoC("channels", logger.C0011) } } } @@ -111,27 +111,27 @@ func (m *Manager) initChannels() error { if m.config.Channels.Feishu.Enabled { feishu, err := NewFeishuChannel(m.config.Channels.Feishu, m.bus) if err != nil { - logger.ErrorCF("channels", "Failed to initialize Feishu channel", map[string]interface{}{ + logger.ErrorCF("channels", logger.C0012, map[string]interface{}{ logger.FieldError: err.Error(), }) } else { m.channels["feishu"] = feishu - logger.InfoC("channels", "Feishu channel enabled successfully") + logger.InfoC("channels", logger.C0013) } } if m.config.Channels.Discord.Enabled { if m.config.Channels.Discord.Token == "" { - logger.WarnC("channels", "Discord token is empty, skipping") + logger.WarnC("channels", logger.C0014) } else { discord, err := NewDiscordChannel(m.config.Channels.Discord, m.bus) if err != nil { - logger.ErrorCF("channels", "Failed to initialize Discord channel", map[string]interface{}{ + logger.ErrorCF("channels", logger.C0015, map[string]interface{}{ logger.FieldError: err.Error(), }) } else { m.channels["discord"] = discord - logger.InfoC("channels", "Discord channel enabled successfully") + logger.InfoC("channels", logger.C0016) } } } @@ -139,44 +139,44 @@ func (m *Manager) initChannels() error { if m.config.Channels.MaixCam.Enabled { maixcam, err := NewMaixCamChannel(m.config.Channels.MaixCam, m.bus) if err != nil { - logger.ErrorCF("channels", "Failed to initialize MaixCam channel", map[string]interface{}{ + logger.ErrorCF("channels", logger.C0017, map[string]interface{}{ logger.FieldError: err.Error(), }) } else { m.channels["maixcam"] = maixcam - logger.InfoC("channels", "MaixCam channel enabled successfully") + logger.InfoC("channels", logger.C0018) } } if m.config.Channels.QQ.Enabled { qq, err := NewQQChannel(m.config.Channels.QQ, m.bus) if err != nil { - logger.ErrorCF("channels", "Failed to initialize QQ channel", map[string]interface{}{ + logger.ErrorCF("channels", logger.C0019, map[string]interface{}{ logger.FieldError: err.Error(), }) } else { m.channels["qq"] = qq - logger.InfoC("channels", "QQ channel enabled successfully") + logger.InfoC("channels", logger.C0020) } } if m.config.Channels.DingTalk.Enabled { if m.config.Channels.DingTalk.ClientID == "" { - logger.WarnC("channels", "DingTalk Client ID is empty, skipping") + logger.WarnC("channels", logger.C0021) } else { dingtalk, err := NewDingTalkChannel(m.config.Channels.DingTalk, m.bus) if err != nil { - logger.ErrorCF("channels", "Failed to initialize DingTalk channel", map[string]interface{}{ + logger.ErrorCF("channels", logger.C0022, map[string]interface{}{ logger.FieldError: err.Error(), }) } else { m.channels["dingtalk"] = dingtalk - logger.InfoC("channels", "DingTalk channel enabled successfully") + logger.InfoC("channels", logger.C0023) } } } - logger.InfoCF("channels", "Channel initialization completed", map[string]interface{}{ + logger.InfoCF("channels", logger.C0024, map[string]interface{}{ "enabled_channels": len(m.channels), }) m.refreshSnapshot() @@ -196,7 +196,7 @@ func (m *Manager) StartAll(ctx context.Context) error { m.mu.Lock() if len(m.channels) == 0 { m.mu.Unlock() - logger.WarnC("channels", "No channels enabled") + logger.WarnC("channels", logger.C0025) return nil } channelsSnapshot := make(map[string]Channel, len(m.channels)) @@ -207,7 +207,7 @@ func (m *Manager) StartAll(ctx context.Context) error { m.dispatchTask = &asyncTask{cancel: cancel} m.mu.Unlock() - logger.InfoC("channels", "Starting all channels") + logger.InfoC("channels", logger.C0026) go m.dispatchOutbound(dispatchCtx) var g errgroup.Group @@ -215,9 +215,9 @@ func (m *Manager) StartAll(ctx context.Context) error { name := name channel := channel g.Go(func() error { - logger.InfoCF("channels", "Starting channel", map[string]interface{}{logger.FieldChannel: name}) + logger.InfoCF("channels", logger.C0027, map[string]interface{}{logger.FieldChannel: name}) if err := channel.Start(ctx); err != nil { - logger.ErrorCF("channels", "Failed to start channel", map[string]interface{}{logger.FieldChannel: name, logger.FieldError: err.Error()}) + logger.ErrorCF("channels", logger.C0028, map[string]interface{}{logger.FieldChannel: name, logger.FieldError: err.Error()}) return fmt.Errorf("%s: %w", name, err) } return nil @@ -226,7 +226,7 @@ func (m *Manager) StartAll(ctx context.Context) error { if err := g.Wait(); err != nil { return err } - logger.InfoC("channels", "All channels started") + logger.InfoC("channels", logger.C0029) return nil } @@ -240,7 +240,7 @@ func (m *Manager) StopAll(ctx context.Context) error { m.dispatchTask = nil m.mu.Unlock() - logger.InfoC("channels", "Stopping all channels") + logger.InfoC("channels", logger.C0030) if task != nil { task.cancel() } @@ -250,9 +250,9 @@ func (m *Manager) StopAll(ctx context.Context) error { name := name channel := channel g.Go(func() error { - logger.InfoCF("channels", "Stopping channel", map[string]interface{}{logger.FieldChannel: name}) + logger.InfoCF("channels", logger.C0031, map[string]interface{}{logger.FieldChannel: name}) if err := channel.Stop(ctx); err != nil { - logger.ErrorCF("channels", "Error stopping channel", map[string]interface{}{logger.FieldChannel: name, logger.FieldError: err.Error()}) + logger.ErrorCF("channels", logger.C0032, map[string]interface{}{logger.FieldChannel: name, logger.FieldError: err.Error()}) return fmt.Errorf("%s: %w", name, err) } return nil @@ -261,7 +261,7 @@ func (m *Manager) StopAll(ctx context.Context) error { if err := g.Wait(); err != nil { return err } - logger.InfoC("channels", "All channels stopped") + logger.InfoC("channels", logger.C0033) return nil } @@ -285,7 +285,7 @@ func (m *Manager) RestartChannel(ctx context.Context, name string) error { return fmt.Errorf("channel %s not found", name) } - logger.InfoCF("channels", "Restarting channel", map[string]interface{}{"channel": name}) + logger.InfoCF("channels", logger.C0034, map[string]interface{}{"channel": name}) _ = channel.Stop(ctx) return channel.Start(ctx) } @@ -335,21 +335,21 @@ func (m *Manager) shouldSkipOutboundDuplicate(msg bus.OutboundMessage) bool { } func (m *Manager) dispatchOutbound(ctx context.Context) { - logger.InfoC("channels", "Outbound dispatcher started") + logger.InfoC("channels", logger.C0035) for { select { case <-ctx.Done(): - logger.InfoC("channels", "Outbound dispatcher stopped") + logger.InfoC("channels", logger.C0036) return default: msg, ok := m.bus.SubscribeOutbound(ctx) if !ok { - logger.InfoC("channels", "Outbound dispatcher stopped (bus closed)") + logger.InfoC("channels", logger.C0037) return } if m.shouldSkipOutboundDuplicate(msg) { - logger.WarnCF("channels", "Duplicate outbound message skipped", map[string]interface{}{ + logger.WarnCF("channels", logger.C0038, map[string]interface{}{ logger.FieldChannel: msg.Channel, logger.FieldChatID: msg.ChatID, }) @@ -365,7 +365,7 @@ func (m *Manager) dispatchOutbound(ctx context.Context) { // Internal/system pseudo channels are not externally dispatchable. continue } - logger.WarnCF("channels", "Unknown channel for outbound message", map[string]interface{}{ + logger.WarnCF("channels", logger.C0039, map[string]interface{}{ logger.FieldChannel: msg.Channel, }) continue @@ -377,9 +377,9 @@ func (m *Manager) dispatchOutbound(ctx context.Context) { } if action != "send" { if ac, ok := channel.(ActionCapable); !ok || !ac.SupportsAction(action) { - logger.WarnCF("channels", "Channel does not support outbound action", map[string]interface{}{ + logger.WarnCF("channels", logger.C0040, map[string]interface{}{ logger.FieldChannel: msg.Channel, - "action": action, + "action": action, }) continue } @@ -387,7 +387,7 @@ func (m *Manager) dispatchOutbound(ctx context.Context) { if m.outboundLimit != nil { if err := m.outboundLimit.Wait(ctx); err != nil { - logger.WarnCF("channels", "Outbound rate limiter canceled", map[string]interface{}{logger.FieldError: err.Error()}) + logger.WarnCF("channels", logger.C0041, map[string]interface{}{logger.FieldError: err.Error()}) continue } } @@ -396,7 +396,7 @@ func (m *Manager) dispatchOutbound(ctx context.Context) { go func(c Channel, outbound bus.OutboundMessage) { defer func() { <-m.dispatchSem }() if err := c.Send(ctx, outbound); err != nil { - logger.ErrorCF("channels", "Error sending message to channel", map[string]interface{}{ + logger.ErrorCF("channels", logger.C0042, map[string]interface{}{ logger.FieldChannel: outbound.Channel, logger.FieldError: err.Error(), }) diff --git a/pkg/channels/qq.go b/pkg/channels/qq.go index 0445bd2..9399645 100644 --- a/pkg/channels/qq.go +++ b/pkg/channels/qq.go @@ -47,7 +47,7 @@ func (c *QQChannel) Start(ctx context.Context) error { return fmt.Errorf("QQ app_id and app_secret not configured") } - logger.InfoC("qq", "Starting QQ bot (WebSocket mode)") + logger.InfoC("qq", logger.C0099) // Create token source credentials := &token.QQBotCredentials{ @@ -80,7 +80,7 @@ func (c *QQChannel) Start(ctx context.Context) error { return fmt.Errorf("failed to get websocket info: %w", err) } - logger.InfoCF("qq", "Got WebSocket info", map[string]interface{}{ + logger.InfoCF("qq", logger.C0100, map[string]interface{}{ "shards": wsInfo.Shards, }) @@ -95,7 +95,7 @@ func (c *QQChannel) Start(ctx context.Context) error { }) c.setRunning(true) - logger.InfoC("qq", "QQ bot started successfully") + logger.InfoC("qq", logger.C0101) return nil } @@ -104,7 +104,7 @@ func (c *QQChannel) Stop(ctx context.Context) error { if !c.IsRunning() { return nil } - logger.InfoC("qq", "Stopping QQ bot") + logger.InfoC("qq", logger.C0102) c.setRunning(false) c.runCancel.cancelAndClear() @@ -124,7 +124,7 @@ func (c *QQChannel) Send(ctx context.Context, msg bus.OutboundMessage) error { // Send C2C message _, err := c.api.PostC2CMessage(ctx, msg.ChatID, msgToCreate) if err != nil { - logger.ErrorCF("qq", "Failed to send C2C message", map[string]interface{}{ + logger.ErrorCF("qq", logger.C0103, map[string]interface{}{ logger.FieldError: err.Error(), }) return err @@ -146,18 +146,18 @@ func (c *QQChannel) handleC2CMessage() event.C2CMessageEventHandler { if data.Author != nil && data.Author.ID != "" { senderID = data.Author.ID } else { - logger.WarnC("qq", "Received message with no sender ID") + logger.WarnC("qq", logger.C0104) return nil } // Extract message content content := data.Content if content == "" { - logger.DebugC("qq", "Received empty message, ignoring") + logger.DebugC("qq", logger.C0105) return nil } - logger.InfoCF("qq", "Received C2C message", map[string]interface{}{ + logger.InfoCF("qq", logger.C0106, map[string]interface{}{ logger.FieldSenderID: senderID, logger.FieldChatID: senderID, logger.FieldMessageContentLength: len(content), @@ -187,18 +187,18 @@ func (c *QQChannel) handleGroupATMessage() event.GroupATMessageEventHandler { if data.Author != nil && data.Author.ID != "" { senderID = data.Author.ID } else { - logger.WarnC("qq", "Received group message with no sender ID") + logger.WarnC("qq", logger.C0107) return nil } // Extract message content (remove bot mention) content := data.Content if content == "" { - logger.DebugC("qq", "Received empty group message, ignoring") + logger.DebugC("qq", logger.C0108) return nil } - logger.InfoCF("qq", "Received group AT message", map[string]interface{}{ + logger.InfoCF("qq", logger.C0109, map[string]interface{}{ logger.FieldSenderID: senderID, "group_id": data.GroupID, logger.FieldMessageContentLength: len(content), diff --git a/pkg/channels/telegram.go b/pkg/channels/telegram.go index 00f38af..6266632 100644 --- a/pkg/channels/telegram.go +++ b/pkg/channels/telegram.go @@ -33,15 +33,15 @@ const ( type TelegramChannel struct { *BaseChannel - bot *telego.Bot - config config.TelegramConfig - chatIDs map[string]int64 - chatIDsMu sync.RWMutex - updates <-chan telego.Update - runCancel cancelGuard - handleSem chan struct{} - handleWG sync.WaitGroup - botUsername string + bot *telego.Bot + config config.TelegramConfig + chatIDs map[string]int64 + chatIDsMu sync.RWMutex + updates <-chan telego.Update + runCancel cancelGuard + handleSem chan struct{} + handleWG sync.WaitGroup + botUsername string } func (c *TelegramChannel) SupportsAction(action string) bool { @@ -62,11 +62,11 @@ func NewTelegramChannel(cfg config.TelegramConfig, bus *bus.MessageBus) (*Telegr base := NewBaseChannel("telegram", cfg, bus, cfg.AllowFrom) return &TelegramChannel{ - BaseChannel: base, - bot: bot, - config: cfg, - chatIDs: make(map[string]int64), - handleSem: make(chan struct{}, telegramMaxConcurrentHandlers), + BaseChannel: base, + bot: bot, + config: cfg, + chatIDs: make(map[string]int64), + handleSem: make(chan struct{}, telegramMaxConcurrentHandlers), }, nil } @@ -91,7 +91,7 @@ func (c *TelegramChannel) Start(ctx context.Context) error { if c.IsRunning() { return nil } - logger.InfoC("telegram", "Starting Telegram bot (polling mode)") + logger.InfoC("telegram", logger.C0054) runCtx, cancel := context.WithCancel(ctx) c.runCancel.set(cancel) @@ -111,7 +111,7 @@ func (c *TelegramChannel) Start(ctx context.Context) error { return fmt.Errorf("failed to get bot info: %w", err) } c.botUsername = strings.ToLower(strings.TrimSpace(botInfo.Username)) - logger.InfoCF("telegram", "Telegram bot connected", map[string]interface{}{ + logger.InfoCF("telegram", logger.C0055, map[string]interface{}{ "username": botInfo.Username, }) @@ -122,7 +122,7 @@ func (c *TelegramChannel) Start(ctx context.Context) error { return case update, ok := <-updates: if !ok { - logger.WarnC("telegram", "Updates channel closed unexpectedly, attempting to restart polling...") + logger.WarnC("telegram", logger.C0056) c.setRunning(false) select { @@ -133,7 +133,7 @@ func (c *TelegramChannel) Start(ctx context.Context) error { newUpdates, err := c.bot.UpdatesViaLongPolling(runCtx, nil) if err != nil { - logger.ErrorCF("telegram", "Failed to restart updates polling", map[string]interface{}{ + logger.ErrorCF("telegram", logger.C0057, map[string]interface{}{ logger.FieldError: err.Error(), }) continue @@ -142,7 +142,7 @@ func (c *TelegramChannel) Start(ctx context.Context) error { updates = newUpdates c.updates = newUpdates c.setRunning(true) - logger.InfoC("telegram", "Updates polling restarted successfully") + logger.InfoC("telegram", logger.C0058) continue } if update.Message != nil { @@ -161,7 +161,7 @@ func (c *TelegramChannel) Stop(ctx context.Context) error { if !c.IsRunning() { return nil } - logger.InfoC("telegram", "Stopping Telegram bot") + logger.InfoC("telegram", logger.C0059) c.setRunning(false) c.runCancel.cancelAndClear() @@ -173,7 +173,7 @@ func (c *TelegramChannel) Stop(ctx context.Context) error { select { case <-done: case <-time.After(telegramStopWaitHandlersPeriod): - logger.WarnC("telegram", "Timeout waiting for telegram message handlers to stop") + logger.WarnC("telegram", logger.C0060) } return nil @@ -195,7 +195,7 @@ func (c *TelegramChannel) dispatchHandleMessage(runCtx context.Context, message defer func() { <-c.handleSem }() defer func() { if r := recover(); r != nil { - logger.ErrorCF("telegram", "Recovered panic in telegram message handler", map[string]interface{}{ + logger.ErrorCF("telegram", logger.C0061, map[string]interface{}{ "panic": fmt.Sprintf("%v", r), }) } @@ -219,7 +219,7 @@ func (c *TelegramChannel) handleCallbackQuery(ctx context.Context, query *telego }) cancel() - logger.InfoCF("telegram", "Callback query received", map[string]interface{}{ + logger.InfoCF("telegram", logger.C0062, map[string]interface{}{ "sender_id": senderID, "data": query.Data, }) @@ -308,7 +308,7 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err cancelSend() if err != nil { - logger.WarnCF("telegram", "HTML parse failed, fallback to plain text", map[string]interface{}{ + logger.WarnCF("telegram", logger.C0063, map[string]interface{}{ logger.FieldError: err.Error(), }) plain := plainTextFromTelegramHTML(htmlContent) @@ -472,7 +472,7 @@ func (c *TelegramChannel) handleMessage(runCtx context.Context, message *telego. return } if user.IsBot { - logger.DebugCF("telegram", "Ignoring bot-originated message", map[string]interface{}{ + logger.DebugCF("telegram", logger.C0064, map[string]interface{}{ "user_id": user.ID, }) return @@ -549,27 +549,27 @@ func (c *TelegramChannel) handleMessage(runCtx context.Context, message *telego. } if !c.isAllowedChat(chatID, message.Chat.Type) { - logger.WarnCF("telegram", "Telegram message rejected by chat allowlist", map[string]interface{}{ + logger.WarnCF("telegram", logger.C0065, map[string]interface{}{ logger.FieldSenderID: senderID, logger.FieldChatID: chatID, }) return } if !c.shouldHandleGroupMessage(message, content) { - logger.DebugCF("telegram", "Ignoring group message without mention/command", map[string]interface{}{ + logger.DebugCF("telegram", logger.C0048, map[string]interface{}{ logger.FieldSenderID: senderID, logger.FieldChatID: chatID, }) return } - logger.InfoCF("telegram", "Telegram message received", map[string]interface{}{ + logger.InfoCF("telegram", logger.C0066, map[string]interface{}{ logger.FieldSenderID: senderID, logger.FieldPreview: truncateString(content, 50), }) if !c.IsAllowed(senderID) { - logger.WarnCF("telegram", "Telegram message rejected by allowlist", map[string]interface{}{ + logger.WarnCF("telegram", logger.C0067, map[string]interface{}{ logger.FieldSenderID: senderID, logger.FieldChatID: chatID, }) @@ -633,7 +633,6 @@ func min(a, b int) int { return b } - func splitTelegramMarkdown(s string, maxRunes int) []string { if s == "" { return []string{""} diff --git a/pkg/channels/utils.go b/pkg/channels/utils.go index adfa5ee..1704e36 100644 --- a/pkg/channels/utils.go +++ b/pkg/channels/utils.go @@ -52,12 +52,14 @@ func runChannelTask(name, taskName string, task func() error, onFailure func(err go func() { if err := task(); err != nil { if errors.Is(err, context.Canceled) { - logger.InfoCF(name, taskName+" stopped", map[string]interface{}{ - "reason": "context canceled", + logger.InfoCF(name, logger.C0168, map[string]interface{}{ + "task_name": taskName, + "reason": "context canceled", }) return } - logger.ErrorCF(name, taskName+" failed", map[string]interface{}{ + logger.ErrorCF(name, logger.C0169, map[string]interface{}{ + "task_name": taskName, logger.FieldError: err.Error(), }) if onFailure != nil { diff --git a/pkg/channels/whatsapp.go b/pkg/channels/whatsapp.go index 5c20866..9e42ac3 100644 --- a/pkg/channels/whatsapp.go +++ b/pkg/channels/whatsapp.go @@ -41,7 +41,7 @@ func (c *WhatsAppChannel) Start(ctx context.Context) error { if c.IsRunning() { return nil } - logger.InfoCF("whatsapp", "Starting WhatsApp channel", map[string]interface{}{ + logger.InfoCF("whatsapp", logger.C0121, map[string]interface{}{ "url": c.url, }) runCtx, cancel := context.WithCancel(ctx) @@ -61,7 +61,7 @@ func (c *WhatsAppChannel) Start(ctx context.Context) error { c.mu.Unlock() c.setRunning(true) - logger.InfoC("whatsapp", "WhatsApp channel connected") + logger.InfoC("whatsapp", logger.C0122) go c.listen(runCtx) @@ -72,7 +72,7 @@ func (c *WhatsAppChannel) Stop(ctx context.Context) error { if !c.IsRunning() { return nil } - logger.InfoC("whatsapp", "Stopping WhatsApp channel") + logger.InfoC("whatsapp", logger.C0123) c.runCancel.cancelAndClear() c.mu.Lock() @@ -80,7 +80,7 @@ func (c *WhatsAppChannel) Stop(ctx context.Context) error { if c.conn != nil { if err := c.conn.Close(); err != nil { - logger.WarnCF("whatsapp", "Error closing WhatsApp connection", map[string]interface{}{ + logger.WarnCF("whatsapp", logger.C0124, map[string]interface{}{ logger.FieldError: err.Error(), }) } @@ -146,12 +146,12 @@ func (c *WhatsAppChannel) listen(ctx context.Context) { return } if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) || errors.Is(err, net.ErrClosed) { - logger.InfoCF("whatsapp", "WhatsApp connection closed", map[string]interface{}{ + logger.InfoCF("whatsapp", logger.C0125, map[string]interface{}{ logger.FieldError: err.Error(), }) return } - logger.WarnCF("whatsapp", "WhatsApp read error", map[string]interface{}{ + logger.WarnCF("whatsapp", logger.C0126, map[string]interface{}{ logger.FieldError: err.Error(), }) if !sleepWithContext(ctx, backoff) { @@ -164,7 +164,7 @@ func (c *WhatsAppChannel) listen(ctx context.Context) { var msg map[string]interface{} if err := json.Unmarshal(message, &msg); err != nil { - logger.WarnCF("whatsapp", "Failed to unmarshal WhatsApp message", map[string]interface{}{ + logger.WarnCF("whatsapp", logger.C0127, map[string]interface{}{ logger.FieldError: err.Error(), }) continue @@ -216,7 +216,7 @@ func (c *WhatsAppChannel) handleIncomingMessage(msg map[string]interface{}) { metadata["user_name"] = userName } - logger.InfoCF("whatsapp", "WhatsApp message received", map[string]interface{}{ + logger.InfoCF("whatsapp", logger.C0128, map[string]interface{}{ logger.FieldSenderID: senderID, logger.FieldPreview: truncateString(content, 50), }) diff --git a/pkg/logger/codes.go b/pkg/logger/codes.go new file mode 100644 index 0000000..308e2fb --- /dev/null +++ b/pkg/logger/codes.go @@ -0,0 +1,179 @@ +package logger + +type CodeID int + +const ( + C0001 CodeID = iota + 1 + C0002 + C0003 + C0004 + C0005 + C0006 + C0007 + C0008 + C0009 + C0010 + C0011 + C0012 + C0013 + C0014 + C0015 + C0016 + C0017 + C0018 + C0019 + C0020 + C0021 + C0022 + C0023 + C0024 + C0025 + C0026 + C0027 + C0028 + C0029 + C0030 + C0031 + C0032 + C0033 + C0034 + C0035 + C0036 + C0037 + C0038 + C0039 + C0040 + C0041 + C0042 + C0043 + C0044 + C0045 + C0046 + C0047 + C0048 + C0049 + C0050 + C0051 + C0052 + C0053 + C0054 + C0055 + C0056 + C0057 + C0058 + C0059 + C0060 + C0061 + C0062 + C0063 + C0064 + C0065 + C0066 + C0067 + C0068 + C0069 + C0070 + C0071 + C0072 + C0073 + C0074 + C0075 + C0076 + C0077 + C0078 + C0079 + C0080 + C0081 + C0082 + C0083 + C0084 + C0085 + C0086 + C0087 + C0088 + C0089 + C0090 + C0091 + C0092 + C0093 + C0094 + C0095 + C0096 + C0097 + C0098 + C0099 + C0100 + C0101 + C0102 + C0103 + C0104 + C0105 + C0106 + C0107 + C0108 + C0109 + C0110 + C0111 + C0112 + C0113 + C0114 + C0115 + C0116 + C0117 + C0118 + C0119 + C0120 + C0121 + C0122 + C0123 + C0124 + C0125 + C0126 + C0127 + C0128 + C0129 + C0130 + C0131 + C0132 + C0133 + C0134 + C0135 + C0136 + C0137 + C0138 + C0139 + C0140 + C0141 + C0142 + C0143 + C0144 + C0145 + C0146 + C0147 + C0148 + C0149 + C0150 + C0151 + C0152 + C0153 + C0154 + C0155 + C0156 + C0157 + C0158 + C0159 + C0160 + C0161 + C0162 + C0163 + C0164 + C0165 + C0166 + C0167 + C0168 + C0169 + C0170 + C0171 + C0172 + C0173 +) diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 91f9348..3c12b83 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "runtime" + "strconv" "strings" "sync" "time" @@ -49,7 +50,8 @@ type LogEntry struct { Level string `json:"level"` Timestamp string `json:"timestamp"` Component string `json:"component,omitempty"` - Message string `json:"message"` + Code int `json:"code,omitempty"` + Message string `json:"message,omitempty"` Fields map[string]interface{} `json:"fields,omitempty"` Caller string `json:"caller,omitempty"` } @@ -106,9 +108,9 @@ func EnableFileLoggingWithRotation(filePath string, maxSizeMB, maxAgeDays int) e logger.maxSizeBytes = int64(maxSizeMB) * 1024 * 1024 logger.maxAgeDays = maxAgeDays if err := logger.cleanupOldLogFiles(); err != nil { - log.Println("Failed to clean up old log files:", err) + log.Println(C0145, err) } - log.Println("File logging enabled:", filePath) + log.Println(C0146, filePath) return nil } @@ -122,20 +124,28 @@ func DisableFileLogging() { logger.filePath = "" logger.maxSizeBytes = 0 logger.maxAgeDays = 0 - log.Println("File logging disabled") + log.Println(C0147) } } -func logMessage(level LogLevel, component string, message string, fields map[string]interface{}) { +func logMessage(level LogLevel, component string, code CodeID, fields map[string]interface{}) { if level < currentLevel { return } + if fields == nil { + fields = map[string]interface{}{} + } + + entryCode := int(code) + entryMessage := extractSystemError(fields) + entry := LogEntry{ Level: logLevelNames[level], Timestamp: time.Now().UTC().Format(time.RFC3339), Component: component, - Message: message, + Code: entryCode, + Message: entryMessage, Fields: fields, } @@ -150,7 +160,7 @@ func logMessage(level LogLevel, component string, message string, fields map[str jsonData, err := json.Marshal(entry) if err == nil { if err := logger.writeLine(append(jsonData, '\n')); err != nil { - log.Println("Failed to write file log:", err) + log.Println(C0148, err) } } } @@ -164,9 +174,12 @@ func logMessage(level LogLevel, component string, message string, fields map[str entry.Timestamp, logLevelNames[level], formatComponent(component), - message, + formatCode(entry.Code), fieldStr, ) + if entry.Message != "" { + logLine += " msg=" + entry.Message + } log.Println(logLine) @@ -175,6 +188,24 @@ func logMessage(level LogLevel, component string, message string, fields map[str } } +func extractSystemError(fields map[string]interface{}) string { + if fields == nil { + return "" + } + v, ok := fields[FieldError] + if !ok || v == nil { + return "" + } + return strings.TrimSpace(fmt.Sprintf("%v", v)) +} + +func formatCode(code int) string { + if code <= 0 { + return "code=0" + } + return "code=" + strconv.Itoa(code) +} + func (l *Logger) writeLine(line []byte) error { l.fileMu.Lock() defer l.fileMu.Unlock() @@ -272,82 +303,82 @@ func formatFields(fields map[string]interface{}) string { return fmt.Sprintf("{%s}", strings.Join(parts, ", ")) } -func Debug(message string) { - logMessage(DEBUG, "", message, nil) +func Debug(code CodeID) { + logMessage(DEBUG, "", code, nil) } -func DebugC(component string, message string) { - logMessage(DEBUG, component, message, nil) +func DebugC(component string, code CodeID) { + logMessage(DEBUG, component, code, nil) } -func DebugF(message string, fields map[string]interface{}) { - logMessage(DEBUG, "", message, fields) +func DebugF(code CodeID, fields map[string]interface{}) { + logMessage(DEBUG, "", code, fields) } -func DebugCF(component string, message string, fields map[string]interface{}) { - logMessage(DEBUG, component, message, fields) +func DebugCF(component string, code CodeID, fields map[string]interface{}) { + logMessage(DEBUG, component, code, fields) } -func Info(message string) { - logMessage(INFO, "", message, nil) +func Info(code CodeID) { + logMessage(INFO, "", code, nil) } -func InfoC(component string, message string) { - logMessage(INFO, component, message, nil) +func InfoC(component string, code CodeID) { + logMessage(INFO, component, code, nil) } -func InfoF(message string, fields map[string]interface{}) { - logMessage(INFO, "", message, fields) +func InfoF(code CodeID, fields map[string]interface{}) { + logMessage(INFO, "", code, fields) } -func InfoCF(component string, message string, fields map[string]interface{}) { - logMessage(INFO, component, message, fields) +func InfoCF(component string, code CodeID, fields map[string]interface{}) { + logMessage(INFO, component, code, fields) } -func Warn(message string) { - logMessage(WARN, "", message, nil) +func Warn(code CodeID) { + logMessage(WARN, "", code, nil) } -func WarnC(component string, message string) { - logMessage(WARN, component, message, nil) +func WarnC(component string, code CodeID) { + logMessage(WARN, component, code, nil) } -func WarnF(message string, fields map[string]interface{}) { - logMessage(WARN, "", message, fields) +func WarnF(code CodeID, fields map[string]interface{}) { + logMessage(WARN, "", code, fields) } -func WarnCF(component string, message string, fields map[string]interface{}) { - logMessage(WARN, component, message, fields) +func WarnCF(component string, code CodeID, fields map[string]interface{}) { + logMessage(WARN, component, code, fields) } -func Error(message string) { - logMessage(ERROR, "", message, nil) +func Error(code CodeID) { + logMessage(ERROR, "", code, nil) } -func ErrorC(component string, message string) { - logMessage(ERROR, component, message, nil) +func ErrorC(component string, code CodeID) { + logMessage(ERROR, component, code, nil) } -func ErrorF(message string, fields map[string]interface{}) { - logMessage(ERROR, "", message, fields) +func ErrorF(code CodeID, fields map[string]interface{}) { + logMessage(ERROR, "", code, fields) } -func ErrorCF(component string, message string, fields map[string]interface{}) { - logMessage(ERROR, component, message, fields) +func ErrorCF(component string, code CodeID, fields map[string]interface{}) { + logMessage(ERROR, component, code, fields) } -func Fatal(message string) { - logMessage(FATAL, "", message, nil) +func Fatal(code CodeID) { + logMessage(FATAL, "", code, nil) } -func FatalC(component string, message string) { - logMessage(FATAL, component, message, nil) +func FatalC(component string, code CodeID) { + logMessage(FATAL, component, code, nil) } -func FatalF(message string, fields map[string]interface{}) { - logMessage(FATAL, "", message, fields) +func FatalF(code CodeID, fields map[string]interface{}) { + logMessage(FATAL, "", code, fields) } -func FatalCF(component string, message string, fields map[string]interface{}) { - logMessage(FATAL, component, message, fields) +func FatalCF(component string, code CodeID, fields map[string]interface{}) { + logMessage(FATAL, component, code, fields) } diff --git a/pkg/providers/http_provider.go b/pkg/providers/http_provider.go index c9bf5b1..98a6971 100644 --- a/pkg/providers/http_provider.go +++ b/pkg/providers/http_provider.go @@ -51,7 +51,7 @@ func (p *HTTPProvider) Chat(ctx context.Context, messages []Message, tools []Too return nil, fmt.Errorf("API base not configured") } - logger.DebugCF("provider", "HTTP chat request", map[string]interface{}{ + logger.DebugCF("provider", logger.C0133, map[string]interface{}{ "api_base": p.apiBase, "protocol": p.protocol, "model": model, @@ -421,7 +421,7 @@ func (p *HTTPProvider) callChatCompletionsStream(ctx context.Context, messages [ } body, _ := json.Marshal(map[string]interface{}{ "choices": []map[string]interface{}{{ - "message": map[string]interface{}{"content": fullText.String()}, + "message": map[string]interface{}{"content": fullText.String()}, "finish_reason": "stop", }}, }) diff --git a/pkg/sentinel/service.go b/pkg/sentinel/service.go index ec342bf..d13ad9d 100644 --- a/pkg/sentinel/service.go +++ b/pkg/sentinel/service.go @@ -17,14 +17,14 @@ import ( type AlertFunc func(msg string) type Service struct { - cfgPath string - workspace string - interval time.Duration - autoHeal bool - onAlert AlertFunc - runner *lifecycle.LoopRunner - mu sync.RWMutex - lastAlerts map[string]time.Time + cfgPath string + workspace string + interval time.Duration + autoHeal bool + onAlert AlertFunc + runner *lifecycle.LoopRunner + mu sync.RWMutex + lastAlerts map[string]time.Time mgr *channels.Manager healingChannels map[string]bool } @@ -34,13 +34,13 @@ func NewService(cfgPath, workspace string, intervalSec int, autoHeal bool, onAle intervalSec = 60 } return &Service{ - cfgPath: cfgPath, - workspace: workspace, - interval: time.Duration(intervalSec) * time.Second, - autoHeal: autoHeal, - onAlert: onAlert, - runner: lifecycle.NewLoopRunner(), - lastAlerts: map[string]time.Time{}, + cfgPath: cfgPath, + workspace: workspace, + interval: time.Duration(intervalSec) * time.Second, + autoHeal: autoHeal, + onAlert: onAlert, + runner: lifecycle.NewLoopRunner(), + lastAlerts: map[string]time.Time{}, healingChannels: map[string]bool{}, } } @@ -53,7 +53,7 @@ func (s *Service) Start() { if !s.runner.Start(s.loop) { return } - logger.InfoCF("sentinel", "Sentinel started", map[string]interface{}{ + logger.InfoCF("sentinel", logger.C0134, map[string]interface{}{ "interval": s.interval.String(), "auto_heal": s.autoHeal, }) @@ -63,7 +63,7 @@ func (s *Service) Stop() { if !s.runner.Stop() { return } - logger.InfoC("sentinel", "Sentinel stopped") + logger.InfoC("sentinel", logger.C0135) } func (s *Service) loop(stopCh <-chan struct{}) { @@ -124,12 +124,12 @@ func (s *Service) checkChannels() []string { delete(s.healingChannels, n) s.mu.Unlock() }() - logger.InfoCF("sentinel", "Attempting auto-heal for channel", map[string]interface{}{"channel": n}) + logger.InfoCF("sentinel", logger.C0136, map[string]interface{}{"channel": n}) // Use a fresh context for restart to avoid being canceled by sentinel loop if rErr := s.mgr.RestartChannel(context.Background(), n); rErr != nil { - logger.ErrorCF("sentinel", "Auto-heal restart failed", map[string]interface{}{"channel": n, "error": rErr.Error()}) + logger.ErrorCF("sentinel", logger.C0137, map[string]interface{}{"channel": n, "error": rErr.Error()}) } else { - logger.InfoCF("sentinel", "Auto-heal successful", map[string]interface{}{"channel": n}) + logger.InfoCF("sentinel", logger.C0138, map[string]interface{}{"channel": n}) } }(name) } @@ -210,7 +210,7 @@ func (s *Service) alert(msg string) { s.lastAlerts[msg] = now s.mu.Unlock() - logger.WarnCF("sentinel", msg, nil) + logger.WarnCF("sentinel", logger.C0170, map[string]interface{}{"alert": msg}) if s.onAlert != nil { s.onAlert(msg) } diff --git a/pkg/server/server.go b/pkg/server/server.go index f81839b..d834ec3 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -32,16 +32,16 @@ func (s *Server) Start() error { Handler: s.withCORS(mux), } - logger.InfoCF("server", "Starting HTTP server", map[string]interface{}{ + logger.InfoCF("server", logger.C0139, map[string]interface{}{ "addr": addr, }) // Check/log indicating it's ready for reverse proxying (per requirement) - logger.InfoC("server", "Server ready for reverse proxying") + logger.InfoC("server", logger.C0140) go func() { if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - logger.ErrorCF("server", "HTTP server failed", map[string]interface{}{ + logger.ErrorCF("server", logger.C0141, map[string]interface{}{ logger.FieldError: err.Error(), }) } @@ -52,7 +52,7 @@ func (s *Server) Start() error { func (s *Server) Stop(ctx context.Context) error { if s.server != nil { - logger.InfoC("server", "Stopping HTTP server") + logger.InfoC("server", logger.C0142) return s.server.Shutdown(ctx) } return nil diff --git a/pkg/tools/registry.go b/pkg/tools/registry.go index 59e23fe..3790653 100644 --- a/pkg/tools/registry.go +++ b/pkg/tools/registry.go @@ -40,7 +40,7 @@ func (r *ToolRegistry) Get(name string) (Tool, bool) { } func (r *ToolRegistry) Execute(ctx context.Context, name string, args map[string]interface{}) (string, error) { - logger.InfoCF("tool", "Tool execution started", + logger.InfoCF("tool", logger.C0164, map[string]interface{}{ "tool": name, "args": args, @@ -48,7 +48,7 @@ func (r *ToolRegistry) Execute(ctx context.Context, name string, args map[string tool, ok := r.Get(name) if !ok { - logger.ErrorCF("tool", "Tool not found", + logger.ErrorCF("tool", logger.C0165, map[string]interface{}{ "tool": name, }) @@ -60,14 +60,14 @@ func (r *ToolRegistry) Execute(ctx context.Context, name string, args map[string duration := time.Since(start) if err != nil { - logger.ErrorCF("tool", "Tool execution failed", + logger.ErrorCF("tool", logger.C0166, map[string]interface{}{ "tool": name, "duration": duration.Milliseconds(), logger.FieldError: err.Error(), }) } else { - logger.InfoCF("tool", "Tool execution completed", + logger.InfoCF("tool", logger.C0167, map[string]interface{}{ "tool": name, "duration_ms": duration.Milliseconds(), diff --git a/webui/public/log-codes.json b/webui/public/log-codes.json new file mode 100644 index 0000000..f7bad98 --- /dev/null +++ b/webui/public/log-codes.json @@ -0,0 +1,698 @@ +{ + "version": 1, + "generated_at": "Tue Mar 3 02:06:08 2026 UTC", + "items": [ + { + "code": 1, + "text": "Message rejected by allowlist" + }, + { + "code": 2, + "text": "Duplicate inbound message skipped" + }, + { + "code": 3, + "text": "Duplicate inbound content skipped" + }, + { + "code": 4, + "text": "Initializing channel manager" + }, + { + "code": 5, + "text": "Attempting to initialize Telegram channel" + }, + { + "code": 6, + "text": "Telegram token is empty, skipping" + }, + { + "code": 7, + "text": "Failed to initialize Telegram channel" + }, + { + "code": 8, + "text": "Telegram channel enabled successfully" + }, + { + "code": 9, + "text": "WhatsApp bridge URL is empty, skipping" + }, + { + "code": 10, + "text": "Failed to initialize WhatsApp channel" + }, + { + "code": 11, + "text": "WhatsApp channel enabled successfully" + }, + { + "code": 12, + "text": "Failed to initialize Feishu channel" + }, + { + "code": 13, + "text": "Feishu channel enabled successfully" + }, + { + "code": 14, + "text": "Discord token is empty, skipping" + }, + { + "code": 15, + "text": "Failed to initialize Discord channel" + }, + { + "code": 16, + "text": "Discord channel enabled successfully" + }, + { + "code": 17, + "text": "Failed to initialize MaixCam channel" + }, + { + "code": 18, + "text": "MaixCam channel enabled successfully" + }, + { + "code": 19, + "text": "Failed to initialize QQ channel" + }, + { + "code": 20, + "text": "QQ channel enabled successfully" + }, + { + "code": 21, + "text": "DingTalk Client ID is empty, skipping" + }, + { + "code": 22, + "text": "Failed to initialize DingTalk channel" + }, + { + "code": 23, + "text": "DingTalk channel enabled successfully" + }, + { + "code": 24, + "text": "Channel initialization completed" + }, + { + "code": 25, + "text": "No channels enabled" + }, + { + "code": 26, + "text": "Starting all channels" + }, + { + "code": 27, + "text": "Starting channel" + }, + { + "code": 28, + "text": "Failed to start channel" + }, + { + "code": 29, + "text": "All channels started" + }, + { + "code": 30, + "text": "Stopping all channels" + }, + { + "code": 31, + "text": "Stopping channel" + }, + { + "code": 32, + "text": "Error stopping channel" + }, + { + "code": 33, + "text": "All channels stopped" + }, + { + "code": 34, + "text": "Restarting channel" + }, + { + "code": 35, + "text": "Outbound dispatcher started" + }, + { + "code": 36, + "text": "Outbound dispatcher stopped" + }, + { + "code": 37, + "text": "Outbound dispatcher stopped (bus closed)" + }, + { + "code": 38, + "text": "Duplicate outbound message skipped" + }, + { + "code": 39, + "text": "Unknown channel for outbound message" + }, + { + "code": 40, + "text": "Channel does not support outbound action" + }, + { + "code": 41, + "text": "Outbound rate limiter canceled" + }, + { + "code": 42, + "text": "Error sending message to channel" + }, + { + "code": 43, + "text": "Feishu channel started (websocket mode)" + }, + { + "code": 44, + "text": "Feishu channel stopped" + }, + { + "code": 45, + "text": "create sheet from markdown table failed" + }, + { + "code": 46, + "text": "Feishu message sent" + }, + { + "code": 47, + "text": "Feishu message rejected by chat allowlist" + }, + { + "code": 48, + "text": "Ignoring group message without mention/command" + }, + { + "code": 49, + "text": "Feishu message received" + }, + { + "code": 50, + "text": "download inbound image failed" + }, + { + "code": 51, + "text": "download inbound file failed" + }, + { + "code": 52, + "text": "set sheet permission failed" + }, + { + "code": 53, + "text": "Upload failed" + }, + { + "code": 54, + "text": "Starting Telegram bot (polling mode)" + }, + { + "code": 55, + "text": "Telegram bot connected" + }, + { + "code": 56, + "text": "Updates channel closed unexpectedly, attempting to restart polling..." + }, + { + "code": 57, + "text": "Failed to restart updates polling" + }, + { + "code": 58, + "text": "Updates polling restarted successfully" + }, + { + "code": 59, + "text": "Stopping Telegram bot" + }, + { + "code": 60, + "text": "Timeout waiting for telegram message handlers to stop" + }, + { + "code": 61, + "text": "Recovered panic in telegram message handler" + }, + { + "code": 62, + "text": "Callback query received" + }, + { + "code": 63, + "text": "HTML parse failed, fallback to plain text" + }, + { + "code": 64, + "text": "Ignoring bot-originated message" + }, + { + "code": 65, + "text": "Telegram message rejected by chat allowlist" + }, + { + "code": 66, + "text": "Telegram message received" + }, + { + "code": 67, + "text": "Telegram message rejected by allowlist" + }, + { + "code": 68, + "text": "Failed to fetch job details" + }, + { + "code": 69, + "text": "Starting Discord bot" + }, + { + "code": 70, + "text": "Discord bot connected" + }, + { + "code": 71, + "text": "Stopping Discord bot" + }, + { + "code": 72, + "text": "Received message" + }, + { + "code": 73, + "text": "Failed to create media directory" + }, + { + "code": 74, + "text": "Failed to download attachment" + }, + { + "code": 75, + "text": "Attachment download returned non-200" + }, + { + "code": 76, + "text": "Failed to create local attachment file" + }, + { + "code": 77, + "text": "Failed to write local attachment file" + }, + { + "code": 78, + "text": "Attachment downloaded successfully" + }, + { + "code": 79, + "text": "Starting MaixCam channel server" + }, + { + "code": 80, + "text": "MaixCam server listening" + }, + { + "code": 81, + "text": "Starting connection acceptor" + }, + { + "code": 82, + "text": "Stopping connection acceptor" + }, + { + "code": 83, + "text": "Failed to accept connection" + }, + { + "code": 84, + "text": "New connection from MaixCam device" + }, + { + "code": 85, + "text": "Handling MaixCam connection" + }, + { + "code": 86, + "text": "Connection closed" + }, + { + "code": 87, + "text": "Failed to decode message" + }, + { + "code": 88, + "text": "Received heartbeat" + }, + { + "code": 89, + "text": "Unknown message type" + }, + { + "code": 90, + "text": "Person detected event" + }, + { + "code": 91, + "text": "Status update from MaixCam" + }, + { + "code": 92, + "text": "Stopping MaixCam channel" + }, + { + "code": 93, + "text": "MaixCam channel stopped" + }, + { + "code": 94, + "text": "No MaixCam devices connected" + }, + { + "code": 95, + "text": "Failed to send to client" + }, + { + "code": 96, + "text": "load recent logs failed" + }, + { + "code": 97, + "text": "Log stream error:" + }, + { + "code": 98, + "text": "Agent initialized" + }, + { + "code": 99, + "text": "Starting QQ bot (WebSocket mode)" + }, + { + "code": 100, + "text": "Got WebSocket info" + }, + { + "code": 101, + "text": "QQ bot started successfully" + }, + { + "code": 102, + "text": "Stopping QQ bot" + }, + { + "code": 103, + "text": "Failed to send C2C message" + }, + { + "code": 104, + "text": "Received message with no sender ID" + }, + { + "code": 105, + "text": "Received empty message, ignoring" + }, + { + "code": 106, + "text": "Received C2C message" + }, + { + "code": 107, + "text": "Received group message with no sender ID" + }, + { + "code": 108, + "text": "Received empty group message, ignoring" + }, + { + "code": 109, + "text": "Received group AT message" + }, + { + "code": 110, + "text": "Startup compaction check completed" + }, + { + "code": 111, + "text": "Bootstrap init model call failed" + }, + { + "code": 112, + "text": "Bootstrap init marker write failed" + }, + { + "code": 113, + "text": "Bootstrap file cleanup failed" + }, + { + "code": 114, + "text": "Bootstrap init model call completed" + }, + { + "code": 115, + "text": "Starting DingTalk channel (Stream Mode)" + }, + { + "code": 116, + "text": "DingTalk channel started (Stream Mode)" + }, + { + "code": 117, + "text": "Stopping DingTalk channel" + }, + { + "code": 118, + "text": "DingTalk channel stopped" + }, + { + "code": 119, + "text": "DingTalk outbound message" + }, + { + "code": 120, + "text": "DingTalk inbound message" + }, + { + "code": 121, + "text": "Starting WhatsApp channel" + }, + { + "code": 122, + "text": "WhatsApp channel connected" + }, + { + "code": 123, + "text": "Stopping WhatsApp channel" + }, + { + "code": 124, + "text": "Error closing WhatsApp connection" + }, + { + "code": 125, + "text": "WhatsApp connection closed" + }, + { + "code": 126, + "text": "WhatsApp read error" + }, + { + "code": 127, + "text": "Failed to unmarshal WhatsApp message" + }, + { + "code": 128, + "text": "WhatsApp message received" + }, + { + "code": 129, + "text": "PublishInbound on closed channel recovered" + }, + { + "code": 130, + "text": "PublishInbound timeout (queue full)" + }, + { + "code": 131, + "text": "PublishOutbound on closed channel recovered" + }, + { + "code": 132, + "text": "PublishOutbound timeout (queue full)" + }, + { + "code": 133, + "text": "HTTP chat request" + }, + { + "code": 134, + "text": "Sentinel started" + }, + { + "code": 135, + "text": "Sentinel stopped" + }, + { + "code": 136, + "text": "Attempting auto-heal for channel" + }, + { + "code": 137, + "text": "Auto-heal restart failed" + }, + { + "code": 138, + "text": "Auto-heal successful" + }, + { + "code": 139, + "text": "Starting HTTP server" + }, + { + "code": 140, + "text": "Server ready for reverse proxying" + }, + { + "code": 141, + "text": "HTTP server failed" + }, + { + "code": 142, + "text": "Stopping HTTP server" + }, + { + "code": 143, + "text": "System prompt built" + }, + { + "code": 144, + "text": "System prompt preview" + }, + { + "code": 145, + "text": "Failed to clean up old log files:" + }, + { + "code": 146, + "text": "File logging enabled:" + }, + { + "code": 147, + "text": "File logging disabled" + }, + { + "code": 148, + "text": "Failed to write file log:" + }, + { + "code": 149, + "text": "Session-sharded dispatcher enabled" + }, + { + "code": 150, + "text": "LLM fallback provider switched" + }, + { + "code": 151, + "text": "LLM iteration" + }, + { + "code": 152, + "text": "LLM request" + }, + { + "code": 153, + "text": "Full LLM request" + }, + { + "code": 154, + "text": "Purged orphan tool outputs after provider pairing error" + }, + { + "code": 155, + "text": "LLM call failed" + }, + { + "code": 156, + "text": "LLM response without tool calls (direct answer)" + }, + { + "code": 157, + "text": "LLM requested tool calls" + }, + { + "code": 158, + "text": "append daily summary log failed" + }, + { + "code": 159, + "text": "Processing system message" + }, + { + "code": 160, + "text": "Purged orphan tool outputs after provider pairing error (system)" + }, + { + "code": 161, + "text": "Heartbeat session reset after repeated provider pairing error" + }, + { + "code": 162, + "text": "LLM call failed in system message" + }, + { + "code": 163, + "text": "System message processing completed" + }, + { + "code": 164, + "text": "Tool execution started" + }, + { + "code": 165, + "text": "Tool not found" + }, + { + "code": 166, + "text": "Tool execution failed" + }, + { + "code": 167, + "text": "Tool execution completed" + }, + { + "code": 168, + "text": "Channel task stopped" + }, + { + "code": 169, + "text": "Channel task failed" + }, + { + "code": 170, + "text": "Sentinel alert" + }, + { + "code": 171, + "text": "Processing inbound message" + }, + { + "code": 172, + "text": "Tool call invoked" + }, + { + "code": 173, + "text": "Response generated" + } + ] +} diff --git a/webui/src/App.tsx b/webui/src/App.tsx index ca78e72..495b087 100644 --- a/webui/src/App.tsx +++ b/webui/src/App.tsx @@ -14,6 +14,7 @@ import Memory from './pages/Memory'; import TaskAudit from './pages/TaskAudit'; import EKG from './pages/EKG'; import Tasks from './pages/Tasks'; +import LogCodes from './pages/LogCodes'; export default function App() { return ( @@ -25,6 +26,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/webui/src/components/Sidebar.tsx b/webui/src/components/Sidebar.tsx index ef86dfb..cc4ec31 100644 --- a/webui/src/components/Sidebar.tsx +++ b/webui/src/components/Sidebar.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { LayoutDashboard, MessageSquare, Settings, Clock, Server, Terminal, Zap, FolderOpen, ClipboardList, ListTodo, BrainCircuit } from 'lucide-react'; +import { LayoutDashboard, MessageSquare, Settings, Clock, Server, Terminal, Zap, FolderOpen, ClipboardList, ListTodo, BrainCircuit, Hash } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useAppContext } from '../context/AppContext'; import NavItem from './NavItem'; @@ -15,6 +15,7 @@ const Sidebar: React.FC = () => { { icon: , label: t('dashboard'), to: '/' }, { icon: , label: t('chat'), to: '/chat' }, { icon: , label: t('logs'), to: '/logs' }, + { icon: , label: t('logCodes'), to: '/log-codes' }, { icon: , label: t('skills'), to: '/skills' }, ], }, diff --git a/webui/src/i18n/index.ts b/webui/src/i18n/index.ts index cde2723..f0c2ed8 100644 --- a/webui/src/i18n/index.ts +++ b/webui/src/i18n/index.ts @@ -11,6 +11,7 @@ const resources = { cronJobs: 'Cron Jobs', nodes: 'Nodes', logs: 'Real-time Logs', + logCodes: 'Log Codes', skills: 'Skills', memory: 'Memory', taskAudit: 'Task Audit', @@ -195,6 +196,7 @@ const resources = { cronJobs: '定时任务', nodes: '节点', logs: '实时日志', + logCodes: '日志编号', skills: '技能管理', memory: '记忆文件', taskAudit: '任务审计', diff --git a/webui/src/pages/Chat.tsx b/webui/src/pages/Chat.tsx index f92ab6a..0676c14 100644 --- a/webui/src/pages/Chat.tsx +++ b/webui/src/pages/Chat.tsx @@ -64,7 +64,7 @@ const Chat: React.FC = () => { const ur = await fetch(`/webui/api/upload${q}`, { method: 'POST', body: fd }); const uj = await ur.json(); media = uj.path || ''; } catch (e) { - console.error('Upload failed', e); + console.error('L0053', e); } } diff --git a/webui/src/pages/Cron.tsx b/webui/src/pages/Cron.tsx index 070f8c3..3b8bf86 100644 --- a/webui/src/pages/Cron.tsx +++ b/webui/src/pages/Cron.tsx @@ -103,7 +103,7 @@ const Cron: React.FC = () => { }); } } catch (e) { - console.error('Failed to fetch job details', e); + console.error('L0068', e); } } else { setEditingCron(null); diff --git a/webui/src/pages/LogCodes.tsx b/webui/src/pages/LogCodes.tsx new file mode 100644 index 0000000..a98fc7c --- /dev/null +++ b/webui/src/pages/LogCodes.tsx @@ -0,0 +1,81 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useAppContext } from '../context/AppContext'; + +type CodeItem = { + code: number; + text: string; +}; + +const LogCodes: React.FC = () => { + const { t } = useTranslation(); + const { q } = useAppContext(); + const [items, setItems] = useState([]); + const [kw, setKw] = useState(''); + + useEffect(() => { + const load = async () => { + try { + const paths = [`/webui/log-codes.json${q}`, '/log-codes.json']; + for (const p of paths) { + const r = await fetch(p); + if (!r.ok) continue; + const j = await r.json(); + if (Array.isArray(j?.items)) { + setItems(j.items); + return; + } + } + } catch { + setItems([]); + } + }; + load(); + }, [q]); + + const filtered = useMemo(() => { + const k = kw.trim().toLowerCase(); + if (!k) return items; + return items.filter((it) => String(it.code).includes(k) || it.text.toLowerCase().includes(k)); + }, [items, kw]); + + return ( +
+
+

{t('logCodes')}

+ setKw(e.target.value)} + placeholder="Search code/text" + className="w-72 bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-indigo-500" + /> +
+ +
+ + + + + + + + + {filtered.map((it) => ( + + + + + ))} + {filtered.length === 0 && ( + + + + )} + +
CodeTemplate
{it.code}{it.text}
No codes
+
+
+ ); +}; + +export default LogCodes; diff --git a/webui/src/pages/Logs.tsx b/webui/src/pages/Logs.tsx index 2a953fa..bb0d811 100644 --- a/webui/src/pages/Logs.tsx +++ b/webui/src/pages/Logs.tsx @@ -8,6 +8,7 @@ const Logs: React.FC = () => { const { t } = useTranslation(); const { q } = useAppContext(); const [logs, setLogs] = useState([]); + const [codeMap, setCodeMap] = useState>({}); const [isStreaming, setIsStreaming] = useState(true); const [showRaw, setShowRaw] = useState(false); const logEndRef = useRef(null); @@ -22,7 +23,7 @@ const Logs: React.FC = () => { setLogs(j.logs.map(normalizeLog)); } } catch (e) { - console.error('load recent logs failed', e); + console.error('L0096', e); } }; @@ -59,11 +60,38 @@ const Logs: React.FC = () => { } } catch (e: any) { if (e.name !== 'AbortError') { - console.error('Log stream error:', e); + console.error('L0097', e); } } }; + const loadCodeMap = async () => { + try { + const paths = [`/webui/log-codes.json${q}`, '/log-codes.json']; + for (const p of paths) { + const r = await fetch(p); + if (!r.ok) continue; + const j = await r.json(); + if (Array.isArray(j?.items)) { + const m: Record = {}; + j.items.forEach((it: any) => { + if (typeof it?.code === 'number' && typeof it?.text === 'string') { + m[it.code] = it.text; + } + }); + setCodeMap(m); + return; + } + } + } catch { + setCodeMap({}); + } + }; + + useEffect(() => { + loadCodeMap(); + }, [q]); + useEffect(() => { loadRecent(); if (isStreaming) { @@ -84,13 +112,29 @@ const Logs: React.FC = () => { const clearLogs = () => setLogs([]); const normalizeLog = (v: any): LogEntry => ({ - time: typeof v?.time === 'string' && v.time ? v.time : new Date().toISOString(), + time: typeof v?.time === 'string' && v.time ? v.time : (typeof v?.timestamp === 'string' && v.timestamp ? v.timestamp : new Date().toISOString()), level: typeof v?.level === 'string' && v.level ? v.level : 'INFO', - msg: typeof v?.msg === 'string' ? v.msg : JSON.stringify(v), + code: typeof v?.code === 'number' ? v.code : undefined, + msg: typeof v?.msg === 'string' ? v.msg : (typeof v?.message === 'string' ? v.message : JSON.stringify(v)), __raw: JSON.stringify(v), ...v, }); + const toCode = (v: any): number | undefined => { + if (typeof v === 'number' && Number.isFinite(v) && v > 0) return v; + if (typeof v === 'string') { + if (/^L\d{4}$/.test(v)) return Number(v.slice(1)); + const n = Number(v); + if (Number.isFinite(n) && n > 0) return n; + } + return undefined; + }; + const decode = (v: any) => { + const c = toCode(v); + if (!c) return v; + return codeMap[c] || v; + }; + const formatTime = (raw: string) => { try { if (!raw) return '--:--:--'; @@ -189,16 +233,19 @@ const Logs: React.FC = () => { {logs.map((log, i) => { const lvl = (log.level || 'INFO').toUpperCase(); - const errText = (log as any).error || (lvl === 'ERROR' ? log.msg : ''); - const message = lvl === 'ERROR' ? ((log as any).message || log.msg || '') : ((log as any).message || log.msg || ''); + const rawCode = (log as any).code ?? (log as any).message ?? log.msg ?? ''; + const message = String(decode(rawCode) || ''); + const errRaw = (log as any).message || (log as any).error || (lvl === 'ERROR' ? rawCode : ''); + const errText = String(decode(errRaw) || ''); const caller = (log as any).caller || (log as any).source || ''; + const code = toCode(rawCode); return ( {formatTime(log.time)} {lvl} {message} {errText} - {caller} + {code ? `${code} | ${caller}` : caller} ); })} diff --git a/webui/src/types/index.ts b/webui/src/types/index.ts index 28eff57..92c5b74 100644 --- a/webui/src/types/index.ts +++ b/webui/src/types/index.ts @@ -25,7 +25,9 @@ export type Lang = 'en' | 'zh'; export type LogEntry = { time: string; level: string; + code?: number; msg: string; + message?: string; [key: string]: any; };