refactor(monitoring): simplify brand dashboard and harden citation rendering
- drop brand_time_buckets and sparkline/coverage UI; dashboard now returns a single day - compute mention rate from total actual samples instead of averaging per-platform rates - canonicalize ai_platform_id across services and aggregate platform-overview snapshots - strip mojibake and non-answer noise from callback payloads and question detail answers - keep Kimi citations panel-only; still store raw search_results for audit - render linked [n] inline citations for qwen/yuanbao answers with scroll-to-source behavior - restrict立即采集 to today's business date with a clear i18n hint - refresh content-citations into an ant-design table with truncation and hover styling Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var monitoringWindows1252ReverseMap = map[rune]byte{
|
||||
0x20AC: 0x80,
|
||||
0x201A: 0x82,
|
||||
0x0192: 0x83,
|
||||
0x201E: 0x84,
|
||||
0x2026: 0x85,
|
||||
0x2020: 0x86,
|
||||
0x2021: 0x87,
|
||||
0x02C6: 0x88,
|
||||
0x2030: 0x89,
|
||||
0x0160: 0x8A,
|
||||
0x2039: 0x8B,
|
||||
0x0152: 0x8C,
|
||||
0x017D: 0x8E,
|
||||
0x2018: 0x91,
|
||||
0x2019: 0x92,
|
||||
0x201C: 0x93,
|
||||
0x201D: 0x94,
|
||||
0x2022: 0x95,
|
||||
0x2013: 0x96,
|
||||
0x2014: 0x97,
|
||||
0x02DC: 0x98,
|
||||
0x2122: 0x99,
|
||||
0x0161: 0x9A,
|
||||
0x203A: 0x9B,
|
||||
0x0153: 0x9C,
|
||||
0x017E: 0x9E,
|
||||
0x0178: 0x9F,
|
||||
}
|
||||
|
||||
func repairMonitoringMojibake(value string) string {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" || !looksLikeMonitoringMojibake(trimmed) {
|
||||
return value
|
||||
}
|
||||
|
||||
repaired, ok := decodeMonitoringLatin1AsUTF8(trimmed)
|
||||
if !ok || strings.ContainsRune(repaired, utf8.RuneError) || !monitoringRepairLooksBetter(trimmed, repaired) {
|
||||
return value
|
||||
}
|
||||
|
||||
return repaired
|
||||
}
|
||||
|
||||
func normalizeMonitoringOptionalString(value *string) *string {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
repaired := repairMonitoringMojibake(*value)
|
||||
return &repaired
|
||||
}
|
||||
|
||||
func looksLikeMonitoringMojibake(value string) bool {
|
||||
if value == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if strings.Contains(value, "Ã") || strings.Contains(value, "â€") || strings.Contains(value, "ï¼") || strings.Contains(value, "ã€") {
|
||||
return true
|
||||
}
|
||||
|
||||
hanCount := monitoringHanRuneCount(value)
|
||||
suspiciousCount := monitoringSuspiciousRuneCount(value)
|
||||
if hanCount > 0 {
|
||||
return suspiciousCount >= 8 && suspiciousCount > hanCount*3
|
||||
}
|
||||
return suspiciousCount >= 4
|
||||
}
|
||||
|
||||
func decodeMonitoringLatin1AsUTF8(value string) (string, bool) {
|
||||
buf := make([]byte, 0, len(value))
|
||||
for _, r := range value {
|
||||
if mapped, ok := monitoringWindows1252ReverseMap[r]; ok {
|
||||
buf = append(buf, mapped)
|
||||
continue
|
||||
}
|
||||
if r > 255 {
|
||||
return "", false
|
||||
}
|
||||
buf = append(buf, byte(r))
|
||||
}
|
||||
if !utf8.Valid(buf) {
|
||||
return "", false
|
||||
}
|
||||
return string(buf), true
|
||||
}
|
||||
|
||||
func monitoringRepairLooksBetter(original, repaired string) bool {
|
||||
if strings.TrimSpace(repaired) == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
originalHan := monitoringHanRuneCount(original)
|
||||
repairedHan := monitoringHanRuneCount(repaired)
|
||||
originalSuspicious := monitoringSuspiciousRuneCount(original)
|
||||
repairedSuspicious := monitoringSuspiciousRuneCount(repaired)
|
||||
|
||||
if repairedHan > originalHan && repairedHan >= 2 {
|
||||
return true
|
||||
}
|
||||
|
||||
return repairedHan > 0 && repairedSuspicious*2 < originalSuspicious
|
||||
}
|
||||
|
||||
func monitoringHanRuneCount(value string) int {
|
||||
count := 0
|
||||
for _, r := range value {
|
||||
if unicode.Is(unicode.Han, r) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func monitoringSuspiciousRuneCount(value string) int {
|
||||
count := 0
|
||||
for _, r := range value {
|
||||
if (r >= 0x00C0 && r <= 0x017F) || (r >= 0x2000 && r <= 0x20FF) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func normalizeMonitoringSourceItemValue(item MonitoringSourceItem) MonitoringSourceItem {
|
||||
item.Title = normalizeMonitoringOptionalString(item.Title)
|
||||
item.SiteName = normalizeMonitoringOptionalString(item.SiteName)
|
||||
item.ResolutionStatus = normalizeMonitoringOptionalString(item.ResolutionStatus)
|
||||
item.ResolutionConfidence = normalizeMonitoringOptionalString(item.ResolutionConfidence)
|
||||
return item
|
||||
}
|
||||
|
||||
func normalizeMonitoringTaskResultRequestValue(req MonitoringTaskResultRequest) MonitoringTaskResultRequest {
|
||||
if req.Answer != nil {
|
||||
repaired := repairMonitoringMojibake(*req.Answer)
|
||||
req.Answer = &repaired
|
||||
}
|
||||
if req.ErrorMessage != nil {
|
||||
repaired := repairMonitoringMojibake(*req.ErrorMessage)
|
||||
req.ErrorMessage = &repaired
|
||||
}
|
||||
|
||||
if len(req.Citations) > 0 {
|
||||
next := make([]MonitoringSourceItem, 0, len(req.Citations))
|
||||
for _, item := range req.Citations {
|
||||
next = append(next, normalizeMonitoringSourceItemValue(item))
|
||||
}
|
||||
req.Citations = next
|
||||
}
|
||||
if len(req.SearchResults) > 0 {
|
||||
next := make([]MonitoringSourceItem, 0, len(req.SearchResults))
|
||||
for _, item := range req.SearchResults {
|
||||
next = append(next, normalizeMonitoringSourceItemValue(item))
|
||||
}
|
||||
req.SearchResults = next
|
||||
}
|
||||
|
||||
if len(req.RawResponseJSON) > 0 {
|
||||
next := make(map[string]interface{}, len(req.RawResponseJSON))
|
||||
for key, value := range req.RawResponseJSON {
|
||||
next[key] = normalizeMonitoringJSONValue(value)
|
||||
}
|
||||
req.RawResponseJSON = next
|
||||
}
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func normalizeMonitoringJSONValue(value interface{}) interface{} {
|
||||
switch typed := value.(type) {
|
||||
case string:
|
||||
return repairMonitoringMojibake(typed)
|
||||
case []interface{}:
|
||||
next := make([]interface{}, len(typed))
|
||||
for index, item := range typed {
|
||||
next[index] = normalizeMonitoringJSONValue(item)
|
||||
}
|
||||
return next
|
||||
case map[string]interface{}:
|
||||
next := make(map[string]interface{}, len(typed))
|
||||
for key, item := range typed {
|
||||
next[key] = normalizeMonitoringJSONValue(item)
|
||||
}
|
||||
return next
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user