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