diff --git a/backend/config/runtime_event.go b/backend/config/runtime_event.go index 311ab98..12dea69 100644 --- a/backend/config/runtime_event.go +++ b/backend/config/runtime_event.go @@ -1,6 +1,7 @@ package config import ( + "regexp" "strconv" "strings" @@ -8,6 +9,50 @@ import ( log "github.com/sirupsen/logrus" ) +var knownActionMap = map[string]string{ + "imap_server.(*serverSession).Login": "IMAP 客户端登录", + "imap_server.(*serverSession).Close": "IMAP 断开连接", + "imap_server.(*serverSession).Select": "IMAP 选择邮箱", + "imap_server.(*serverSession).Fetch": "IMAP 获取邮件", + "imap_server.(*serverSession).Append": "IMAP 追加邮件", + "imap_server.(*serverSession).Delete": "IMAP 删除邮箱", + "pop3_server.action.Pass": "POP3 客户端登录", + "pop3_server.action.Retr": "POP3 获取邮件", + "smtp_server.(*Session).AuthPlain": "SMTP 客户端认证", + "send.doSend": "SMTP 邮件投递", + "send.doSend.func2": "SMTP 邮件投递", + "send.Send": "发送邮件", +} + +var cleanFuncRe = regexp.MustCompile(`^([a-zA-Z0-9_]+)\.(?:\(\*?[a-zA-Z0-9_]+\)\.)?([a-zA-Z0-9_]+)(?:\.func\d+)?$`) + +func friendlyActionFromCaller(function string) string { + if action, ok := knownActionMap[function]; ok { + return action + } + m := cleanFuncRe.FindStringSubmatch(function) + if len(m) == 3 { + pkg := m[1] + fn := m[2] + pkgLabel := map[string]string{ + "imap_server": "IMAP 服务", + "pop3_server": "POP3 服务", + "smtp_server": "SMTP 服务", + "send": "邮件投递", + "controllers": "接口处理", + "services": "系统服务", + "res_init": "系统初始化", + "hooks": "插件钩子", + "adminreset": "超级管理", + }[pkg] + if pkgLabel == "" { + pkgLabel = pkg + } + return pkgLabel + " · " + fn + } + return "运行事件 · " + function +} + // runtimeEventFields 为文件日志提供稳定的事件边界;没有完成信号时只记为状态记录。 // 不从任意消息中的“成功”或日志级别猜测业务结果,也不改变调用方的字段。 func runtimeEventFields(entry *log.Entry) map[string]any { @@ -30,7 +75,7 @@ func runtimeEventFields(entry *log.Entry) map[string]any { if index := strings.LastIndex(function, "/"); index >= 0 { function = function[index+1:] } - action += " · " + function + action = friendlyActionFromCaller(function) } fields["action"] = action } @@ -38,4 +83,4 @@ func runtimeEventFields(entry *log.Entry) map[string]any { fields["result"] = "recorded" } return fields -} +} \ No newline at end of file diff --git a/backend/listen/imap_server/session_login.go b/backend/listen/imap_server/session_login.go index dedce73..149c672 100644 --- a/backend/listen/imap_server/session_login.go +++ b/backend/listen/imap_server/session_login.go @@ -10,7 +10,11 @@ import ( func (s *serverSession) Login(username, pwd string) error { user, err := auth.AuthenticateProtocol("imap", s.remoteAddress, username, pwd) if err != nil { - log.WithContext(s.ctx).Info("IMAP authentication failed") + log.WithContext(s.ctx).WithFields(log.Fields{ + "action": "IMAP 客户端登录", + "result": "failure", + "object": s.remoteAddress, + }).Info("IMAP 客户端认证失败") return &imap.Error{ Type: imap.StatusResponseTypeNo, Code: imap.ResponseCodeAuthenticationFailed, @@ -24,6 +28,10 @@ func (s *serverSession) Login(username, pwd string) error { s.ctx.UserAccount = user.Account s.ctx.Role = user.EffectiveRole() s.ctx.IsAdmin = s.ctx.Role == access.RoleSuperAdmin || s.ctx.Role == access.RoleAdmin - log.WithContext(s.ctx).Debug("IMAP authentication succeeded") + log.WithContext(s.ctx).WithFields(log.Fields{ + "action": "IMAP 客户端登录", + "result": "success", + "object": s.remoteAddress, + }).Debug("IMAP 客户端认证成功") return nil -} +} \ No newline at end of file diff --git a/backend/listen/pop3_server/action.go b/backend/listen/pop3_server/action.go index 2a73036..51cff51 100644 --- a/backend/listen/pop3_server/action.go +++ b/backend/listen/pop3_server/action.go @@ -105,7 +105,11 @@ func (a action) Pass(session *gopop.Session, pwd string) error { } user, err := auth.AuthenticateProtocol("pop3", remoteAddress, session.User, pwd) if err != nil { - log.WithContext(session.Ctx).Info("POP3 authentication failed") + log.WithContext(session.Ctx).WithFields(log.Fields{ + "action": "POP3 客户端登录", + "result": "failure", + "object": remoteAddress, + }).Info("POP3 客户端认证失败") return errors2.New("password error") } diff --git a/backend/listen/smtp_server/action.go b/backend/listen/smtp_server/action.go index 1b59dd6..cc07e50 100644 --- a/backend/listen/smtp_server/action.go +++ b/backend/listen/smtp_server/action.go @@ -64,7 +64,11 @@ func (s *Session) Auth(mech string) (sasl.Server, error) { func (s *Session) AuthPlain(username, pwd string) error { user, err := auth.AuthenticateProtocol("smtp", s.RemoteAddress.String(), username, pwd) if err != nil { - log.WithContext(s.Ctx).Info("SMTP authentication failed") + log.WithContext(s.Ctx).WithFields(log.Fields{ + "action": "SMTP 客户端认证", + "result": "failure", + "object": s.RemoteAddress.String(), + }).Info("SMTP 客户端认证失败") return errors.New("password error") } diff --git a/backend/utils/send/send.go b/backend/utils/send/send.go index 06dd30f..651176d 100644 --- a/backend/utils/send/send.go +++ b/backend/utils/send/send.go @@ -277,7 +277,11 @@ func doSend(ctx *context.Context, fromDomain string, data []byte, to []*parsemai recordFailure(err) return } - log.WithContext(ctx).Infof("SMTP STARTTLS on 25 Send Error. %s", err.Error()) + log.WithContext(ctx).WithFields(log.Fields{ + "action": "SMTP 邮件投递", + "result": "failure", + "object": domain.mxHost + ":25", + }).Infof("SMTP 25 端口 STARTTLS 投递失败: %s", err.Error()) // 再试用587投递 err = smtp.SendMailWithTls(strings.TrimSuffix(domain.mxHost, "."), domain.mxHost+":587", nil, from, fromDomain, buildAddress(tos), data) @@ -293,7 +297,11 @@ func doSend(ctx *context.Context, fromDomain string, data []byte, to []*parsemai recordFailure(err) return } - log.WithContext(ctx).Infof("SMTPS on 587 Send Error. %s", err.Error()) + log.WithContext(ctx).WithFields(log.Fields{ + "action": "SMTP 邮件投递", + "result": "failure", + "object": domain.mxHost + ":587", + }).Infof("SMTP 587 端口投递失败: %s", err.Error()) // 再次尝试465投递 err = smtp.SendMailWithTls(strings.TrimSuffix(domain.mxHost, "."), domain.mxHost+":465", nil, from, fromDomain, buildAddress(tos), data) @@ -309,7 +317,11 @@ func doSend(ctx *context.Context, fromDomain string, data []byte, to []*parsemai recordFailure(err) return } - log.WithContext(ctx).Infof("SMTPS on 465 Send Error. %s", err.Error()) + log.WithContext(ctx).WithFields(log.Fields{ + "action": "SMTP 邮件投递", + "result": "failure", + "object": domain.mxHost + ":465", + }).Infof("SMTP 465 端口投递失败: %s", err.Error()) // 所有安全投递方式均失败后记录错误,不允许回退到明文连接。 recordFailure(err) diff --git a/frontend/src/views/components/LogSettings/LogSettings.vue b/frontend/src/views/components/LogSettings/LogSettings.vue index 3cc4d9d..9bb6203 100644 --- a/frontend/src/views/components/LogSettings/LogSettings.vue +++ b/frontend/src/views/components/LogSettings/LogSettings.vue @@ -179,4 +179,4 @@ const { } = useLogs() - + \ No newline at end of file diff --git a/frontend/src/views/components/LogSettings/RuntimeLog.js b/frontend/src/views/components/LogSettings/RuntimeLog.js index 6480367..2564915 100644 --- a/frontend/src/views/components/LogSettings/RuntimeLog.js +++ b/frontend/src/views/components/LogSettings/RuntimeLog.js @@ -22,6 +22,86 @@ export function timeParts(value) { return { date: localDate.format(date), time: localTime.format(date) } } +export function cleanRuntimeMessage(text) { + if (!text) return '' + const boundary = text.lastIndexOf(' @event ') + if (boundary >= 0) { + return text.slice(0, boundary).trim() + } + return text +} + +export function friendlyOutcomeText(text) { + if (!text) return '(无详细说明)' + const trimmed = text.trim() + const dictionary = { + 'IMAP authentication failed': 'IMAP 客户端登录认证失败', + 'IMAP authentication succeeded': 'IMAP 客户端登录认证成功', + 'POP3 authentication failed': 'POP3 客户端登录认证失败', + 'POP3 authentication succeeded': 'POP3 客户端登录认证成功', + 'SMTP authentication failed': 'SMTP 客户端登录认证失败', + 'SMTP authentication succeeded': 'SMTP 客户端登录认证成功', + 'SMTP MAIL accepted': 'SMTP 接收发件人地址', + 'SMTP RCPT accepted': 'SMTP 接收收件人地址', + 'Plugin Stop': '停止插件执行', + 'Server Start Success': '服务启动成功', + '开始绑定监听端口': '开始绑定并监听网络端口' + } + if (dictionary[trimmed]) return dictionary[trimmed] + + if (/^SMTP STARTTLS on 25 Send Error\.\s*(.*)$/i.test(trimmed)) { + const detail = trimmed.replace(/^SMTP STARTTLS on 25 Send Error\.\s*/i, '') + return `SMTP 25 端口 STARTTLS 投递失败:${detail}` + } + if (/^SMTPS on 587 Send Error\.\s*(.*)$/i.test(trimmed)) { + const detail = trimmed.replace(/^SMTPS on 587 Send Error\.\s*/i, '') + return `SMTP 587 端口投递失败:${detail}` + } + if (/^SMTPS on 465 Send Error\.\s*(.*)$/i.test(trimmed)) { + const detail = trimmed.replace(/^SMTPS on 465 Send Error\.\s*/i, '') + return `SMTP 465 端口投递失败:${detail}` + } + return trimmed +} + +export function friendlyActionText(action) { + if (!action) return '历史运行事件' + const known = { + '运行事件 · imap_server.(*serverSession).Login': 'IMAP 客户端登录', + '运行事件 · imap_server.(*serverSession).Close': 'IMAP 断开连接', + '运行事件 · imap_server.(*serverSession).Select': 'IMAP 选择邮箱', + '运行事件 · imap_server.(*serverSession).Fetch': 'IMAP 获取邮件', + '运行事件 · imap_server.(*serverSession).Append': 'IMAP 追加邮件', + '运行事件 · imap_server.(*serverSession).Delete': 'IMAP 删除邮箱', + '运行事件 · pop3_server.action.Pass': 'POP3 客户端登录', + '运行事件 · pop3_server.action.Retr': 'POP3 获取邮件', + '运行事件 · smtp_server.(*Session).AuthPlain': 'SMTP 客户端认证', + '运行事件 · send.doSend': 'SMTP 邮件投递', + '运行事件 · send.doSend.func2': 'SMTP 邮件投递', + '运行事件 · send.Send': '发送邮件' + } + if (known[action]) return known[action] + + const m = action.match(/^运行事件\s*·\s*([a-zA-Z0-9_]+)\.(?:\(\*?[a-zA-Z0-9_]+\)\.)?([a-zA-Z0-9_]+)(?:\.func\d+)?$/) + if (m) { + const pkg = m[1] + const fn = m[2] + const pkgLabel = { + imap_server: 'IMAP 服务', + pop3_server: 'POP3 服务', + smtp_server: 'SMTP 服务', + send: '邮件投递', + controllers: '接口处理', + services: '系统服务', + res_init: '系统初始化', + hooks: '插件钩子', + adminreset: '超级管理' + }[pkg] || pkg + return `${pkgLabel} · ${fn}` + } + return action +} + // 只识别已知的日志头;未知格式原样展示,不丢弃记录或猜测级别。 export function parseRuntimeLine(line, sequence) { const raw = String(line ?? '') @@ -67,10 +147,17 @@ export function parseRuntimeLine(line, sequence) { // 旧日志或截断记录保留原文,不能猜测结果。 } } + const displayMessage = cleanRuntimeMessage(message) + const rawAction = event.action || '历史运行事件' + const action = friendlyActionText(rawAction) + const rawOutcome = event.outcome || content + const outcome = friendlyOutcomeText(rawOutcome) + return { - sequence, raw, createdAt, level, source, sourceShort, requestId, message, summary, - action: event.action || '历史运行事件', result: event.result || 'unknown', - object: event.object || event.section || '', outcome: event.outcome || content, + sequence, raw, createdAt, level, source, sourceShort, requestId, + message: displayMessage, summary, + action, result: event.result || 'unknown', + object: event.object || event.section || '', outcome, ...timeParts(createdAt), ...levels[level] } } @@ -90,4 +177,4 @@ export function formatFileSize(value) { if (bytes < 1024) return `${bytes} B` if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` return `${(bytes / (1024 * 1024)).toFixed(1)} MB` -} +} \ No newline at end of file