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:
@@ -1683,6 +1683,8 @@ func (s *MonitoringCallbackService) upsertParseResult(ctx context.Context, tx pg
|
||||
}
|
||||
|
||||
func (s *MonitoringCallbackService) processTaskResult(ctx context.Context, task *monitoringCollectTask, req MonitoringTaskResultRequest) (*MonitoringTaskResultResponse, error) {
|
||||
req = normalizeMonitoringTaskResultRequestValue(req)
|
||||
|
||||
runStatus := normalizeRunStatus(req.Status)
|
||||
if runStatus == "" {
|
||||
return nil, response.ErrBadRequest(40041, "invalid_status", "status must be succeeded or failed")
|
||||
@@ -1703,7 +1705,7 @@ func (s *MonitoringCallbackService) processTaskResult(ctx context.Context, task
|
||||
completedAt = req.CompletedAt.UTC().Round(0)
|
||||
}
|
||||
|
||||
rawPayload, sourceInputs := buildMonitoringRawPayload(req)
|
||||
rawPayload, sourceInputs := buildMonitoringRawPayload(task.AIPlatformID, req)
|
||||
|
||||
tx, err := s.monitoringPool.Begin(ctx)
|
||||
if err != nil {
|
||||
@@ -2280,7 +2282,7 @@ func (s *MonitoringCallbackService) PatchTaskResultQueueMeta(taskID int64, recei
|
||||
s.tryPatchTaskResultQueueMeta(taskID, receivedAt, patch)
|
||||
}
|
||||
|
||||
func buildMonitoringRawPayload(req MonitoringTaskResultRequest) ([]byte, []MonitoringSourceItem) {
|
||||
func buildMonitoringRawPayload(platformID string, req MonitoringTaskResultRequest) ([]byte, []MonitoringSourceItem) {
|
||||
payload := make(map[string]interface{}, len(req.RawResponseJSON)+4)
|
||||
for key, value := range req.RawResponseJSON {
|
||||
payload[key] = value
|
||||
@@ -2299,16 +2301,7 @@ func buildMonitoringRawPayload(req MonitoringTaskResultRequest) ([]byte, []Monit
|
||||
payload["status"] = strings.TrimSpace(req.Status)
|
||||
}
|
||||
|
||||
sourceInputs := append([]MonitoringSourceItem{}, req.Citations...)
|
||||
sourceInputs = append(sourceInputs, req.SearchResults...)
|
||||
sourceInputs = append(sourceInputs, extractMonitoringSourceItems(payload["citations"])...)
|
||||
sourceInputs = append(sourceInputs, extractMonitoringSourceItems(payload["references"])...)
|
||||
sourceInputs = append(sourceInputs, extractMonitoringSourceItems(payload["sources"])...)
|
||||
sourceInputs = append(sourceInputs, extractMonitoringSourceItems(payload["thought_references"])...)
|
||||
sourceInputs = append(sourceInputs, extractMonitoringSourceItems(payload["thinking_references"])...)
|
||||
sourceInputs = append(sourceInputs, extractMonitoringSourceItems(payload["search_results"])...)
|
||||
sourceInputs = append(sourceInputs, extractMonitoringSourceItems(payload["searchResults"])...)
|
||||
|
||||
sourceInputs := buildMonitoringCitationSourceInputs(platformID, req, payload)
|
||||
if len(payload) == 0 {
|
||||
return nil, sourceInputs
|
||||
}
|
||||
@@ -2320,6 +2313,34 @@ func buildMonitoringRawPayload(req MonitoringTaskResultRequest) ([]byte, []Monit
|
||||
return data, sourceInputs
|
||||
}
|
||||
|
||||
func buildMonitoringCitationSourceInputs(platformID string, req MonitoringTaskResultRequest, payload map[string]interface{}) []MonitoringSourceItem {
|
||||
includeSearchResults := !monitoringPlatformUsesDedicatedCitationPanel(platformID)
|
||||
|
||||
sourceInputs := append([]MonitoringSourceItem{}, req.Citations...)
|
||||
if includeSearchResults {
|
||||
sourceInputs = append(sourceInputs, req.SearchResults...)
|
||||
}
|
||||
sourceInputs = append(sourceInputs, extractMonitoringSourceItems(payload["citations"])...)
|
||||
sourceInputs = append(sourceInputs, extractMonitoringSourceItems(payload["references"])...)
|
||||
sourceInputs = append(sourceInputs, extractMonitoringSourceItems(payload["sources"])...)
|
||||
sourceInputs = append(sourceInputs, extractMonitoringSourceItems(payload["thought_references"])...)
|
||||
sourceInputs = append(sourceInputs, extractMonitoringSourceItems(payload["thinking_references"])...)
|
||||
if includeSearchResults {
|
||||
sourceInputs = append(sourceInputs, extractMonitoringSourceItems(payload["search_results"])...)
|
||||
sourceInputs = append(sourceInputs, extractMonitoringSourceItems(payload["searchResults"])...)
|
||||
}
|
||||
return sourceInputs
|
||||
}
|
||||
|
||||
func monitoringPlatformUsesDedicatedCitationPanel(platformID string) bool {
|
||||
switch normalizeMonitoringPlatformID(platformID) {
|
||||
case "kimi":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func newMonitoringTaskResultEnvelope(task *monitoringCollectTask, installationID string, receivedAt time.Time, req MonitoringTaskResultRequest) monitoringTaskResultEnvelope {
|
||||
return monitoringTaskResultEnvelope{
|
||||
Version: "v1",
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildMonitoringRawPayloadKimiExcludesSearchResultsFromCitationInputs(t *testing.T) {
|
||||
citationTitle := "真实引用文章"
|
||||
searchTitle := "搜索网页结果"
|
||||
|
||||
req := MonitoringTaskResultRequest{
|
||||
Status: "succeeded",
|
||||
Citations: []MonitoringSourceItem{
|
||||
{
|
||||
URL: "https://example.com/citation",
|
||||
Title: &citationTitle,
|
||||
},
|
||||
},
|
||||
SearchResults: []MonitoringSourceItem{
|
||||
{
|
||||
URL: "https://example.com/search",
|
||||
Title: &searchTitle,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rawPayload, sourceInputs := buildMonitoringRawPayload("kimi", req)
|
||||
if len(sourceInputs) != 1 {
|
||||
t.Fatalf("buildMonitoringRawPayload() sourceInputs len = %d, want 1", len(sourceInputs))
|
||||
}
|
||||
for _, item := range sourceInputs {
|
||||
if item.URL != "https://example.com/citation" {
|
||||
t.Fatalf("buildMonitoringRawPayload() source url = %q, want citation url", item.URL)
|
||||
}
|
||||
}
|
||||
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(rawPayload, &payload); err != nil {
|
||||
t.Fatalf("json.Unmarshal(rawPayload) error = %v", err)
|
||||
}
|
||||
if got := len(extractMonitoringSourceItems(payload["search_results"])); got != 1 {
|
||||
t.Fatalf("stored search_results len = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildMonitoringRawPayloadQwenIncludesSearchResultsInCitationInputs(t *testing.T) {
|
||||
searchTitle := "搜索网页结果"
|
||||
|
||||
req := MonitoringTaskResultRequest{
|
||||
Status: "succeeded",
|
||||
SearchResults: []MonitoringSourceItem{
|
||||
{
|
||||
URL: "https://example.com/search",
|
||||
Title: &searchTitle,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, sourceInputs := buildMonitoringRawPayload("qwen", req)
|
||||
if len(sourceInputs) != 1 {
|
||||
t.Fatalf("buildMonitoringRawPayload() sourceInputs len = %d, want 1", len(sourceInputs))
|
||||
}
|
||||
for _, item := range sourceInputs {
|
||||
if item.URL != "https://example.com/search" {
|
||||
t.Fatalf("buildMonitoringRawPayload() source url = %q, want search url", item.URL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractMonitoringInlineCitationsFromRawResponseKimi(t *testing.T) {
|
||||
raw := []byte(`{
|
||||
"inline_citation_candidates": [
|
||||
{
|
||||
"url": "https://example.com/a#ref",
|
||||
"title": "文章 A",
|
||||
"site_name": "示例站点"
|
||||
},
|
||||
{
|
||||
"url": "https://example.com/a#other",
|
||||
"title": "文章 A",
|
||||
"site_name": "示例站点"
|
||||
},
|
||||
{
|
||||
"url": "https://example.com/b",
|
||||
"title": "文章 B",
|
||||
"site_name": "另一个站点"
|
||||
}
|
||||
]
|
||||
}`)
|
||||
|
||||
items := extractMonitoringInlineCitationsFromRawResponse(raw, "kimi")
|
||||
if len(items) != 2 {
|
||||
t.Fatalf("extractMonitoringInlineCitationsFromRawResponse() len = %d, want 2", len(items))
|
||||
}
|
||||
if items[0].URL != "https://example.com/a" {
|
||||
t.Fatalf("extractMonitoringInlineCitationsFromRawResponse() first url = %q, want https://example.com/a", items[0].URL)
|
||||
}
|
||||
if items[1].URL != "https://example.com/b" {
|
||||
t.Fatalf("extractMonitoringInlineCitationsFromRawResponse() second url = %q, want https://example.com/b", items[1].URL)
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ func (s *MonitoringCallbackService) rebuildBrandDailyProjection(ctx context.Cont
|
||||
|
||||
desiredSampleCount := enabledQuestionCount * int64(len(platforms))
|
||||
coverageRate := divideAsPointer(brandAgg.ActualSampleCount, brandAgg.PlannedSampleCount)
|
||||
mentionRate := averageMonitoringPlatformMentionRate(platformAggs)
|
||||
mentionRate := divideAsPointer(brandAgg.MentionedCount, brandAgg.ActualSampleCount)
|
||||
top1MentionRate := divideAsPointer(brandAgg.Top1MentionedCount, brandAgg.ActualSampleCount)
|
||||
firstRecommendRate := divideAsPointer(brandAgg.FirstRecommendedCount, brandAgg.ActualSampleCount)
|
||||
positiveMentionRate := divideAsPointer(brandAgg.PositiveMentionedCount, brandAgg.ActualSampleCount)
|
||||
@@ -151,7 +151,7 @@ func loadMonitoringProjectionPlatforms(ctx context.Context, businessPool *pgxpoo
|
||||
if len(enabledJSON) > 0 {
|
||||
var configured []string
|
||||
if jsonErr := json.Unmarshal(enabledJSON, &configured); jsonErr == nil && len(configured) > 0 {
|
||||
enabled = configured
|
||||
enabled = monitoringPlatformIDs(defaultMonitoringPlatforms)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,29 +372,6 @@ func (s *MonitoringCallbackService) loadMonitoringPlatformDailyAggregates(ctx co
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func averageMonitoringPlatformMentionRate(items []monitoringPlatformDailyAggregate) *float64 {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
total := 0.0
|
||||
count := 0
|
||||
for _, item := range items {
|
||||
rate := divideAsPointer(item.MentionedCount, item.ActualSampleCount)
|
||||
if rate == nil {
|
||||
continue
|
||||
}
|
||||
total += *rate
|
||||
count++
|
||||
}
|
||||
if count == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
average := total / float64(count)
|
||||
return &average
|
||||
}
|
||||
|
||||
func (s *MonitoringCallbackService) replaceMonitoringPlatformDailyProjection(ctx context.Context, tx pgx.Tx, tenantID, brandID int64, businessDate time.Time, items []monitoringPlatformDailyAggregate) error {
|
||||
dateText := monitoringBusinessDateText(businessDate)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -93,3 +93,68 @@ func TestFilterMonitoringPlatformsFallsBackToDisplayName(t *testing.T) {
|
||||
{ID: "doubao", Name: "豆包"},
|
||||
}, filtered)
|
||||
}
|
||||
|
||||
func TestNormalizeMonitoringPlatformIDCanonicalizesLatestIDs(t *testing.T) {
|
||||
assert.Equal(t, "qwen", normalizeMonitoringPlatformID(" QWEN "))
|
||||
assert.Equal(t, "wenxin", normalizeMonitoringPlatformID("WenXin"))
|
||||
assert.Equal(t, "yuanbao", normalizeMonitoringPlatformID("yuanbao"))
|
||||
}
|
||||
|
||||
func TestFilterMonitoringPlatformsMatchesCanonicalPlatform(t *testing.T) {
|
||||
platforms := []monitoringPlatformMetadata{
|
||||
{ID: "qwen", Name: "通义千问"},
|
||||
}
|
||||
selected := "qwen"
|
||||
|
||||
filtered := filterMonitoringPlatforms(platforms, &selected)
|
||||
|
||||
assert.Equal(t, []monitoringPlatformMetadata{
|
||||
{ID: "qwen", Name: "通义千问"},
|
||||
}, filtered)
|
||||
}
|
||||
|
||||
func TestIntersectMonitoringPlatformsAcceptsCanonicalIDs(t *testing.T) {
|
||||
enabled := []monitoringPlatformMetadata{
|
||||
{ID: "qwen", Name: "通义千问"},
|
||||
{ID: "wenxin", Name: "文心一言"},
|
||||
}
|
||||
|
||||
filtered, err := intersectMonitoringPlatforms(enabled, []string{"qwen", "wenxin"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []monitoringPlatformMetadata{
|
||||
{ID: "qwen", Name: "通义千问"},
|
||||
{ID: "wenxin", Name: "文心一言"},
|
||||
}, filtered)
|
||||
}
|
||||
|
||||
func TestReconcileEnabledMonitoringPlatformsBackfillsSupportedCatalog(t *testing.T) {
|
||||
assert.Equal(t, []string{"yuanbao", "kimi", "wenxin", "deepseek", "doubao", "qwen"}, reconcileEnabledMonitoringPlatforms([]string{"deepseek", "qwen", "doubao"}))
|
||||
}
|
||||
|
||||
func TestApplyDerivedRatesToOverviewSnapshotUsesAllSamplesForMentionRate(t *testing.T) {
|
||||
day := time.Date(2026, 4, 22, 0, 0, 0, 0, monitoringBusinessLocation())
|
||||
snapshot := monitoringOverviewSnapshot{Date: day}
|
||||
derived := newMonitoringDerivedMetrics()
|
||||
derived.ByDate["2026-04-22"] = monitoringDerivedRateStats{
|
||||
Total: 4,
|
||||
MentionedCount: 1,
|
||||
Top1Count: 1,
|
||||
FirstRecommend: 1,
|
||||
PositiveMentions: 1,
|
||||
}
|
||||
derived.ByDatePlatform["2026-04-22"] = map[string]monitoringDerivedRateStats{
|
||||
"qwen": {Total: 1, MentionedCount: 1},
|
||||
"deepseek": {Total: 3, MentionedCount: 0},
|
||||
}
|
||||
|
||||
applyDerivedRatesToOverviewSnapshot(&snapshot, derived)
|
||||
|
||||
require.True(t, snapshot.MentionRate.Valid)
|
||||
require.True(t, snapshot.Top1MentionRate.Valid)
|
||||
require.True(t, snapshot.FirstRecommendRate.Valid)
|
||||
require.True(t, snapshot.PositiveMentionRate.Valid)
|
||||
assert.InDelta(t, 0.25, snapshot.MentionRate.Float64, 0.0001)
|
||||
assert.InDelta(t, 0.25, snapshot.Top1MentionRate.Float64, 0.0001)
|
||||
assert.InDelta(t, 0.25, snapshot.FirstRecommendRate.Float64, 0.0001)
|
||||
assert.InDelta(t, 0.25, snapshot.PositiveMentionRate.Float64, 0.0001)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package app
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestRepairMonitoringMojibake(t *testing.T) {
|
||||
input := "用户询问åˆè‚¥å…¨å±‹å®šåˆ¶çš„æ€§ä»·æ¯”最高的å“牌"
|
||||
got := repairMonitoringMojibake(input)
|
||||
want := "用户询问合肥全屋定制的性价比最高的品牌"
|
||||
|
||||
if got != want {
|
||||
t.Fatalf("repairMonitoringMojibake() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepairMonitoringMojibakeLeavesNormalChineseUntouched(t *testing.T) {
|
||||
input := "用户询问合肥全屋定制的性价比最高的品牌"
|
||||
got := repairMonitoringMojibake(input)
|
||||
if got != input {
|
||||
t.Fatalf("repairMonitoringMojibake() changed valid text: got %q, want %q", got, input)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user