chore: 并入 iotlcm-bot 历史到 bot/(原提交 31dbc1d)

This commit is contained in:
MiFaZhan
2026-11-02 13:28:00 +08:00
36 changed files with 3006 additions and 0 deletions
+187
View File
@@ -0,0 +1,187 @@
package render
import (
"encoding/json"
"fmt"
"strings"
"time"
"git.5iot.com/iotlcm-public/iotlcm-bot/internal/archive"
)
// Kind 返回卡片标题用的中文名称。流量同步有日、月历史、本月快照三条链路,
// 标题必须能区分,否则运营无法从卡片判断是哪一类同步。
func Kind(operation string) string {
switch operation {
case "previous_day_usage_full_sync":
return "日流量同步"
case "previous_month_usage_full_sync":
return "月历史同步"
case "current_month_usage_full_sync":
return "本月流量快照同步"
}
if strings.Contains(operation, "sim_status") {
return "卡状态全量同步"
}
if isTrafficUsage(operation) {
return "流量全量同步"
}
return operation
}
// isTrafficUsage 判定 operation 是否属于流量族(日、月历史、本月快照及兼容旧操作名)。
// 供料观测等按链路区分的行为用它判断,避免依赖 Kind 返回的中文标题。
func isTrafficUsage(operation string) bool {
return strings.Contains(operation, "traffic") || strings.Contains(operation, "usage")
}
func Text(a archive.RunArchive, environment string) string {
title := fmt.Sprintf("【%s|%s】", environment, Kind(a.Run.Operation))
startAt, endAt := executionTimes(a)
lines := []string{
title,
"",
fmt.Sprintf("总数:%s", number(a.TotalCount())),
fmt.Sprintf("成功:%s", number(a.Run.SuccessCount)),
fmt.Sprintf("失败:%s", number(a.Run.FailedCount)),
fmt.Sprintf("成功率:%.2f%%", a.SuccessRate()),
fmt.Sprintf("开始时间:%s", timestamp(startAt)),
fmt.Sprintf("结束时间:%s", timestamp(endAt)),
fmt.Sprintf("总耗时:%s", duration(a.Duration())),
fmt.Sprintf("实际执行:%d 轮", len(a.Rounds)),
"",
"轮次结果:",
}
for _, r := range a.Rounds {
lines = append(lines, roundLine(r))
}
if block := supplyBlock(a); block != "" {
lines = append(lines, "", block)
}
lines = append(lines, "", "Run:"+a.Run.RunID)
return strings.Join(lines, "\n")
}
func Card(a archive.RunArchive, environment string) ([]byte, error) {
title := fmt.Sprintf("%s · %s", displayEnvironment(environment), Kind(a.Run.Operation))
startAt, endAt := executionTimes(a)
// Full sync runs commonly have final failures; use one neutral header color.
template := "blue"
elements := []any{
fields(
field("总数", number(a.TotalCount())),
field("成功", number(a.Run.SuccessCount)),
field("失败", number(a.Run.FailedCount)),
field("成功率", fmt.Sprintf("%.2f%%", a.SuccessRate())),
field("开始时间", timestamp(startAt)),
field("结束时间", timestamp(endAt)),
field("总耗时", duration(a.Duration())),
field("实际轮次", fmt.Sprintf("%d轮", len(a.Rounds))),
),
map[string]any{"tag": "hr"},
}
for _, r := range a.Rounds {
elements = append(elements, map[string]any{
"tag": "div",
"text": map[string]any{"tag": "lark_md", "content": roundBlock(r)},
})
}
if block := supplyBlock(a); block != "" {
elements = append(elements, map[string]any{"tag": "div", "text": map[string]any{"tag": "lark_md", "content": block}})
}
elements = append(elements,
map[string]any{"tag": "hr"},
map[string]any{"tag": "note", "elements": []any{map[string]any{"tag": "plain_text", "content": "Run: " + a.Run.RunID}}},
)
card := map[string]any{
"msg_type": "interactive",
"card": map[string]any{
"header": map[string]any{"template": template, "title": map[string]any{"tag": "plain_text", "content": title}},
"elements": elements,
},
}
return json.Marshal(card)
}
func fields(values ...map[string]any) map[string]any {
entries := make([]any, len(values))
for i, value := range values {
entries[i] = value
}
return map[string]any{"tag": "div", "fields": entries}
}
func field(label, value string) map[string]any {
return map[string]any{
"is_short": true,
"text": map[string]any{"tag": "lark_md", "content": fmt.Sprintf("**%s**\n%s", label, value)},
}
}
func roundBlock(r archive.RoundStats) string {
return fmt.Sprintf("**第 %d 轮 · %s 张 · %s**\n成功 %s(%.2f%%)\n进入下轮 %s · 终态失败 %s", r.Round, number(r.EnqueuedCount), duration(r.Duration()), number(r.SuccessCount), r.SuccessRate(), number(r.NextRoundCount), number(r.FailedCount))
}
func displayEnvironment(environment string) string {
switch environment {
case "preview":
return "预演"
case "prod", "production":
return "生产"
default:
return environment
}
}
func roundLine(r archive.RoundStats) string {
return fmt.Sprintf("第 %d 轮|%s 张|成功 %s(%.2f%%)|进入下轮 %s|终态失败 %s|%s", r.Round, number(r.EnqueuedCount), number(r.SuccessCount), r.SuccessRate(), number(r.NextRoundCount), number(r.FailedCount), duration(r.Duration()))
}
func executionTimes(a archive.RunArchive) (string, string) {
if len(a.Rounds) == 0 {
return a.Run.CreatedAt, a.Run.CompletedAt
}
return a.Rounds[0].StartedAt, a.Rounds[len(a.Rounds)-1].CompletedAt
}
func timestamp(value string) string {
t, err := time.Parse(time.RFC3339Nano, value)
if err != nil {
return value
}
return t.In(time.FixedZone("CST", 8*60*60)).Format("2006-01-02 15:04:05")
}
func number(n int64) string {
negative := n < 0
if negative {
n = -n
}
s := fmt.Sprintf("%d", n)
for i := len(s) - 3; i > 0; i -= 3 {
s = s[:i] + "," + s[i:]
}
if negative {
return "-" + s
}
return s
}
func duration(d time.Duration) string {
seconds := int64(d / time.Second)
if seconds < 0 {
return "未记录"
}
h, m, s := seconds/3600, (seconds%3600)/60, seconds%60
parts := make([]string, 0, 3)
if h > 0 {
parts = append(parts, fmt.Sprintf("%d小时", h))
}
if m > 0 {
parts = append(parts, fmt.Sprintf("%d分", m))
}
if s > 0 || len(parts) == 0 {
parts = append(parts, fmt.Sprintf("%d秒", s))
}
return strings.Join(parts, "")
}