fix(monitoring): reject succeeded results missing answers or citation sources
Server callback now rejects succeeded submissions without a non-empty answer, or whose answer contains [citation:N] markers without any sources. Desktop adapters mirror this client-side: kimi returns unknown when sources arrive without a final answer, and yuanbao returns unknown when the answer has unresolved citation markers. Yuanbao also unwraps redirect URLs, harvests citations from more DOM attributes and document URL fields, drops links pointing back at yuanbao itself, and dedupes overlapping streaming fragments. Runtime controller forwards only an explicit "succeeded" status as success; anything else becomes failed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -40,6 +41,8 @@ const (
|
||||
monitoringHeartbeatAccessSnapshotRefreshAfter = 10 * time.Minute
|
||||
)
|
||||
|
||||
var monitoringCitationMarkerPattern = regexp.MustCompile(`(?i)\[\s*citation\s*:\s*\d+\s*\]`)
|
||||
|
||||
type MonitoringCallbackService struct {
|
||||
businessPool *pgxpool.Pool
|
||||
monitoringPool *pgxpool.Pool
|
||||
@@ -390,9 +393,13 @@ func (s *MonitoringCallbackService) handleTaskResultForInstallation(
|
||||
return nil, response.ErrConflict(40941, "task_status_invalid", "task status does not allow result submission")
|
||||
}
|
||||
|
||||
if normalizeRunStatus(req.Status) == "" {
|
||||
runStatus := normalizeRunStatus(req.Status)
|
||||
if runStatus == "" {
|
||||
return nil, response.ErrBadRequest(40041, "invalid_status", "status must be succeeded or failed")
|
||||
}
|
||||
if err := validateMonitoringTaskResultSubmission(task.AIPlatformID, runStatus, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
receivedAt := time.Now().UTC().Round(0)
|
||||
envelope := newMonitoringTaskResultEnvelope(task, installation.ID, receivedAt, req)
|
||||
@@ -2851,6 +2858,23 @@ func normalizeRunStatus(value string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func validateMonitoringTaskResultSubmission(platformID string, runStatus string, req MonitoringTaskResultRequest) error {
|
||||
if runStatus != "succeeded" {
|
||||
return nil
|
||||
}
|
||||
answer := strings.TrimSpace(monitoringStringValue(req.Answer))
|
||||
if answer == "" {
|
||||
return response.ErrBadRequest(40041, "invalid_monitoring_result_answer", "succeeded monitoring result requires a non-empty answer")
|
||||
}
|
||||
if monitoringCitationMarkerPattern.MatchString(answer) {
|
||||
_, sourceInputs := buildMonitoringRawPayload(platformID, req)
|
||||
if len(sourceInputs) == 0 {
|
||||
return response.ErrBadRequest(40041, "invalid_monitoring_result_citations", "succeeded monitoring result with citation markers requires citation sources")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeMonitoringLeaseLimit(value int) int {
|
||||
if value <= 0 {
|
||||
return defaultMonitoringLeaseLimit
|
||||
|
||||
@@ -327,6 +327,42 @@ func TestBuildMonitoringRawPayloadKimiIncludesSearchResultsInCitationInputs(t *t
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateMonitoringTaskResultSubmissionRequiresAnswerOnSuccess(t *testing.T) {
|
||||
if err := validateMonitoringTaskResultSubmission("kimi", "failed", MonitoringTaskResultRequest{Status: "failed"}); err != nil {
|
||||
t.Fatalf("validateMonitoringTaskResultSubmission() failed status error = %v, want nil", err)
|
||||
}
|
||||
|
||||
answer := "有效回答"
|
||||
if err := validateMonitoringTaskResultSubmission("kimi", "succeeded", MonitoringTaskResultRequest{Status: "succeeded", Answer: &answer}); err != nil {
|
||||
t.Fatalf("validateMonitoringTaskResultSubmission() succeeded status with answer error = %v, want nil", err)
|
||||
}
|
||||
|
||||
if err := validateMonitoringTaskResultSubmission("kimi", "succeeded", MonitoringTaskResultRequest{Status: "succeeded"}); err == nil {
|
||||
t.Fatal("validateMonitoringTaskResultSubmission() succeeded status without answer error = nil, want error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateMonitoringTaskResultSubmissionRequiresSourcesForCitationMarkers(t *testing.T) {
|
||||
answer := "正文 [citation:1]"
|
||||
if err := validateMonitoringTaskResultSubmission("yuanbao", "succeeded", MonitoringTaskResultRequest{
|
||||
Status: "succeeded",
|
||||
Answer: &answer,
|
||||
}); err == nil {
|
||||
t.Fatal("validateMonitoringTaskResultSubmission() citation marker without sources error = nil, want error")
|
||||
}
|
||||
|
||||
title := "引用文章"
|
||||
if err := validateMonitoringTaskResultSubmission("yuanbao", "succeeded", MonitoringTaskResultRequest{
|
||||
Status: "succeeded",
|
||||
Answer: &answer,
|
||||
Citations: []MonitoringSourceItem{
|
||||
{URL: "https://example.com/source", Title: &title},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("validateMonitoringTaskResultSubmission() citation marker with sources error = %v, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildMonitoringRawPayloadQwenIncludesSearchResultsInCitationInputs(t *testing.T) {
|
||||
searchTitle := "搜索网页结果"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user