This commit is contained in:
lpf
2026-03-03 10:36:53 +08:00
parent 35b0ad1bfd
commit bd93c12edc
30 changed files with 1311 additions and 262 deletions

View File

@@ -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"],

View File

@@ -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"],

View File

@@ -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,
})

View File

@@ -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),

View File

@@ -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,
})

View File

@@ -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,
})

View File

@@ -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,

View File

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

View File

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

View File

@@ -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(),
})

View File

@@ -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(),
})

View File

@@ -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),

View File

@@ -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{""}

View File

@@ -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 {

View File

@@ -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),
})

179
pkg/logger/codes.go Normal file
View File

@@ -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
)

View File

@@ -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)
}

View File

@@ -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",
}},
})

View File

@@ -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)
}

View File

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

View File

@@ -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(),

698
webui/public/log-codes.json Normal file
View File

@@ -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"
}
]
}

View File

@@ -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() {
<Route index element={<Dashboard />} />
<Route path="chat" element={<Chat />} />
<Route path="logs" element={<Logs />} />
<Route path="log-codes" element={<LogCodes />} />
<Route path="skills" element={<Skills />} />
<Route path="config" element={<Config />} />
<Route path="cron" element={<Cron />} />

View File

@@ -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: <LayoutDashboard className="w-5 h-5" />, label: t('dashboard'), to: '/' },
{ icon: <MessageSquare className="w-5 h-5" />, label: t('chat'), to: '/chat' },
{ icon: <Terminal className="w-5 h-5" />, label: t('logs'), to: '/logs' },
{ icon: <Hash className="w-5 h-5" />, label: t('logCodes'), to: '/log-codes' },
{ icon: <Zap className="w-5 h-5" />, label: t('skills'), to: '/skills' },
],
},

View File

@@ -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: '任务审计',

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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<CodeItem[]>([]);
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 (
<div className="p-8 max-w-7xl mx-auto space-y-6">
<div className="flex items-center justify-between gap-3">
<h1 className="text-2xl font-semibold tracking-tight">{t('logCodes')}</h1>
<input
value={kw}
onChange={(e) => 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"
/>
</div>
<div className="bg-zinc-950 border border-zinc-800 rounded-2xl overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-zinc-900/90 border-b border-zinc-800">
<tr className="text-zinc-400">
<th className="text-left p-3 font-medium w-40">Code</th>
<th className="text-left p-3 font-medium">Template</th>
</tr>
</thead>
<tbody>
{filtered.map((it) => (
<tr key={it.code} className="border-b border-zinc-900 hover:bg-zinc-900/40">
<td className="p-3 font-mono text-indigo-300">{it.code}</td>
<td className="p-3 text-zinc-200 break-all">{it.text}</td>
</tr>
))}
{filtered.length === 0 && (
<tr>
<td className="p-6 text-zinc-500" colSpan={2}>No codes</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
);
};
export default LogCodes;

View File

@@ -8,6 +8,7 @@ const Logs: React.FC = () => {
const { t } = useTranslation();
const { q } = useAppContext();
const [logs, setLogs] = useState<LogEntry[]>([]);
const [codeMap, setCodeMap] = useState<Record<number, string>>({});
const [isStreaming, setIsStreaming] = useState(true);
const [showRaw, setShowRaw] = useState(false);
const logEndRef = useRef<HTMLDivElement>(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<number, string> = {};
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 = () => {
<tbody>
{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 (
<tr key={i} className="border-b border-zinc-900 hover:bg-zinc-900/40 align-top">
<td className="p-2 text-zinc-500 whitespace-nowrap">{formatTime(log.time)}</td>
<td className={`p-2 font-semibold whitespace-nowrap ${getLevelColor(lvl)}`}>{lvl}</td>
<td className="p-2 text-zinc-200 break-all">{message}</td>
<td className="p-2 text-red-300 break-all">{errText}</td>
<td className="p-2 text-zinc-500 break-all">{caller}</td>
<td className="p-2 text-zinc-500 break-all">{code ? `${code} | ${caller}` : caller}</td>
</tr>
);
})}

View File

@@ -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;
};