164 lines
6.0 KiB
Go
164 lines
6.0 KiB
Go
package service
|
|
|
|
import (
|
|
"html"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// notifyTplDef 描述一类通知的模板元数据与示例变量(测试发送用)。
|
|
// 所有模板额外提供公共变量 {{time}}(发送时刻)。
|
|
type notifyTplDef struct {
|
|
Label string
|
|
Default string
|
|
Vars []string
|
|
Sample map[string]string
|
|
}
|
|
|
|
// notifyTplOrder 是模板列表的稳定输出顺序(与通知管理事件一致)。
|
|
var notifyTplOrder = []string{
|
|
"task_fail", "task_recover", "snatch_success", "tenant_dead", "task_stop",
|
|
"login_lock", "model_deprecated",
|
|
"log_event_instance", "log_event_identity", "log_event_policy", "log_event_region", "log_event_login",
|
|
}
|
|
|
|
var notifyTplDefs = map[string]notifyTplDef{
|
|
"task_fail": {
|
|
Label: "任务失败",
|
|
Default: "❌ 任务失败:{{task_name}}\n{{error}}",
|
|
Vars: []string{"task_name", "error"},
|
|
Sample: map[string]string{"task_name": "示例测活任务", "error": "鉴权失败: 401 NotAuthenticated"},
|
|
},
|
|
"task_recover": {
|
|
Label: "任务恢复",
|
|
Default: "✅ 任务恢复:{{task_name}}",
|
|
Vars: []string{"task_name"},
|
|
Sample: map[string]string{"task_name": "示例测活任务"},
|
|
},
|
|
"snatch_success": {
|
|
Label: "抢机成功",
|
|
Default: "🎉 抢机成功:{{task_name}}\n{{message}}",
|
|
Vars: []string{"task_name", "message"},
|
|
Sample: map[string]string{"task_name": "示例抢机任务", "message": "已创建 1/1 台实例"},
|
|
},
|
|
"tenant_dead": {
|
|
Label: "租户失联",
|
|
Default: "⚠️ 租户失联:{{tenants}}",
|
|
Vars: []string{"tenants"},
|
|
Sample: map[string]string{"tenants": "免费01、免费02"},
|
|
},
|
|
"task_stop": {
|
|
Label: "任务停止",
|
|
Default: "⛔ 任务已停止:{{task_name}}\n{{error}}",
|
|
Vars: []string{"task_name", "error"},
|
|
Sample: map[string]string{"task_name": "示例抢机任务", "error": "连续鉴权失败 5 次,任务熔断"},
|
|
},
|
|
"login_lock": {
|
|
Label: "登录锁定",
|
|
Default: "🚫 登录锁定:用户 {{username}}(来自 {{ip}})连续失败 {{fail_count}} 次,已锁定 {{lock_minutes}} 分钟",
|
|
Vars: []string{"username", "ip", "fail_count", "lock_minutes"},
|
|
Sample: map[string]string{"username": "admin", "ip": "203.0.113.8", "fail_count": "5", "lock_minutes": "15"},
|
|
},
|
|
"model_deprecated": {
|
|
Label: "模型弃用预警",
|
|
Default: "⚠️ AI 网关:以下在池模型即将被 OCI 退役或弃用(退役后无法调用,弃用仅为预告)\n{{models}}",
|
|
Vars: []string{"models"},
|
|
Sample: map[string]string{"models": "meta.llama-3.1-70b-instruct(2026-08-01 退役,届时无法调用)"},
|
|
},
|
|
"log_event_instance": {
|
|
Label: "实例生命周期",
|
|
Default: "☁️ 云端事件:{{tenant}} {{event}} {{resource}}",
|
|
Vars: []string{"tenant", "event", "resource"},
|
|
Sample: map[string]string{"tenant": "免费01", "event": "TerminateInstance", "resource": "web-server-1"},
|
|
},
|
|
"log_event_identity": {
|
|
Label: "用户与凭据",
|
|
Default: "☁️ 云端事件:{{tenant}} {{event}} {{resource}}",
|
|
Vars: []string{"tenant", "event", "resource"},
|
|
Sample: map[string]string{"tenant": "免费01", "event": "CreateApiKey", "resource": "user/demo"},
|
|
},
|
|
"log_event_policy": {
|
|
Label: "策略变更",
|
|
Default: "☁️ 云端事件:{{tenant}} {{event}} {{resource}}",
|
|
Vars: []string{"tenant", "event", "resource"},
|
|
Sample: map[string]string{"tenant": "免费01", "event": "UpdatePolicy", "resource": "admin-policy"},
|
|
},
|
|
"log_event_region": {
|
|
Label: "区域订阅",
|
|
Default: "☁️ 云端事件:{{tenant}} {{event}} {{resource}}",
|
|
Vars: []string{"tenant", "event", "resource"},
|
|
Sample: map[string]string{"tenant": "免费01", "event": "CreateRegionSubscription", "resource": "ap-osaka-1"},
|
|
},
|
|
"log_event_login": {
|
|
Label: "控制台登录",
|
|
Default: "☁️ 云端事件:{{tenant}} {{event}} {{resource}}",
|
|
Vars: []string{"tenant", "event", "resource"},
|
|
Sample: map[string]string{"tenant": "免费01", "event": "InteractiveLogin", "resource": "demo@example.com"},
|
|
},
|
|
}
|
|
|
|
var notifyVarPattern = regexp.MustCompile(`\{\{(\w+)\}\}`)
|
|
|
|
// renderNotifyTemplate 用 {{var}} 占位符渲染模板;未知变量原样保留,
|
|
// 公共变量 time 自动注入(调用方未提供时)。
|
|
func renderNotifyTemplate(tpl string, vars map[string]string) string {
|
|
if _, ok := vars["time"]; !ok {
|
|
if vars == nil {
|
|
vars = map[string]string{}
|
|
}
|
|
vars["time"] = time.Now().Format("2006-01-02 15:04:05")
|
|
}
|
|
return notifyVarPattern.ReplaceAllStringFunc(tpl, func(m string) string {
|
|
if v, ok := vars[m[2:len(m)-2]]; ok {
|
|
return v
|
|
}
|
|
return m
|
|
})
|
|
}
|
|
|
|
var (
|
|
notifyMdPre = regexp.MustCompile("(?s)```\n?(.+?)\n?```")
|
|
notifyMdCode = regexp.MustCompile("`([^`\n]+)`")
|
|
notifyMdBold = regexp.MustCompile(`\*([^*\n]+)\*`)
|
|
notifyMdItalic = regexp.MustCompile(`_([^_\n]+)_`)
|
|
)
|
|
|
|
// mdToTelegramHTML 把轻量 Markdown(*粗体* / _斜体_ / `代码` / ```代码块```)转为
|
|
// Telegram HTML;先整体 HTML 转义,未闭合标记原样保留,天然不产生残缺标签。
|
|
func mdToTelegramHTML(text string) string {
|
|
out := html.EscapeString(text)
|
|
out = notifyMdPre.ReplaceAllString(out, "<pre>$1</pre>")
|
|
out = notifyMdCode.ReplaceAllString(out, "<code>$1</code>")
|
|
out = notifyMdBold.ReplaceAllString(out, "<b>$1</b>")
|
|
out = notifyMdItalic.ReplaceAllString(out, "<i>$1</i>")
|
|
return out
|
|
}
|
|
|
|
// NotifyTemplateView 是模板设置接口的列表项。
|
|
type NotifyTemplateView struct {
|
|
Kind string `json:"kind"`
|
|
Label string `json:"label"`
|
|
Vars []string `json:"vars"`
|
|
DefaultTemplate string `json:"defaultTemplate"`
|
|
// Template 是自定义模板;空串表示未自定义(按默认发送)
|
|
Template string `json:"template"`
|
|
}
|
|
|
|
// notifyTplKey 是 kind 对应的 Setting 键。
|
|
func notifyTplKey(kind string) string { return "notify_tpl_" + kind }
|
|
|
|
// notifyTplValid 报告 kind 是否为已知通知类型。
|
|
func notifyTplValid(kind string) bool {
|
|
_, ok := notifyTplDefs[kind]
|
|
return ok
|
|
}
|
|
|
|
// notifyTplText 返回 kind 生效模板:自定义非空用自定义,否则默认。
|
|
func notifyTplText(custom, kind string) string {
|
|
if strings.TrimSpace(custom) != "" {
|
|
return custom
|
|
}
|
|
return notifyTplDefs[kind].Default
|
|
}
|