fix(yuanbao): track new hunyuan_t1 SSE protocol and align inline citations with DeepSeek

- findInteractiveByText now also accepts elements carrying business
  attributes [dt-button-id]/[data-button-id]. The new yuanbao toolbar
  renders the deep-think toggle as <div dt-button-id="deep_think"> rather
  than a <button>, so the previous text-only locator missed it and probes
  silently ran in the fast model — without inline citations.
- Lower <div data-idx-list="1,2,10"> placeholders to bare "[1][2][10]"
  (previously "[citation:1] [citation:2] [citation:10]") and accept both
  forms in YUANBAO_CITATION_MARKER_PATTERN. Final answer text runs through
  normalizeCitationMarkers so any residual SSE [citation:N] is rewritten
  to [N], matching DeepSeek's chip rendering on the SaaS side.
- searchGuid frames in the new protocol carry every reference inside
  docs[] (each with a 1-based index that mirrors the answer markers) and
  citations is now always null. Promote docs into the citations bucket so
  citations[index-1] aligns with the inline marker, eliminating the
  spurious yuanbao_incomplete_citations failures.
- Update the SaaS detail view: createYuanbaoCitationPattern accepts both
  "[N]" and "[citation:N]" so yuanbao answers render the same clickable
  superscript chips as DeepSeek.

Tests cover the new searchGuid.docs alignment, fast-cache answers (no
markers, no sources -> still succeed), legacy [citation:N] -> [N]
rewriting, and mixed-format marker extraction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 21:51:24 +08:00
parent af35301d9b
commit 1e6ed77453
3 changed files with 173 additions and 6 deletions
@@ -10,7 +10,14 @@ const YUANBAO_QUERY_TIMEOUT_MS = 120_000
const YUANBAO_CAPTURE_LIMIT = 96
const YUANBAO_CAPTURE_BODY_LIMIT = 1_200_000
const YUANBAO_CAPTURE_FLUSH_TIMEOUT_MS = 20_000
const YUANBAO_CITATION_MARKER_PATTERN = /\[citation:\s*(\d+)\]/gi
// Match both formats so we tolerate either side of the migration without breaking:
// - "[citation:N]" — emitted by the legacy yuanbao SSE protocol and what older
// captures / fixtures still contain.
// - "[N]" — emitted by the new model (DOM places <div data-idx-list="N,M,..."/>
// placeholders inline; we now lower them to bare "[N]" markers, which is also
// the format DeepSeek answers carry through to the SaaS side).
const YUANBAO_CITATION_MARKER_PATTERN = /\[(?:citation:\s*)?(\d+)\]/gi
const YUANBAO_LEGACY_CITATION_MARKER_PATTERN = /\[citation:\s*(\d+)\]/gi
const WINDOWS_1252_REVERSE_MAP = new Map<number, number>([
[0x20ac, 0x80],
[0x201a, 0x82],
@@ -205,6 +212,16 @@ function extractCitationMarkerIndexes(value: string | null): number[] {
return indexes
}
// Normalise legacy "[citation:N]" markers (still emitted by some SSE frames)
// to "[N]" so the SaaS front-end can render the same superscript chip as
// DeepSeek answers — without that, mixed sources show two unrelated formats.
function normalizeCitationMarkers(value: string | null): string | null {
if (value == null) {
return value
}
return value.replace(YUANBAO_LEGACY_CITATION_MARKER_PATTERN, '[$1]')
}
function looksLikeNonAnswerNoise(value: string): boolean {
const trimmed = value.trim()
if (!trimmed) {
@@ -1041,6 +1058,14 @@ function walkYuanbaoPayload(value: unknown, summary: YuanbaoSummary, depth = 0):
)
break
case 'searchGuid':
// The new hunyuan_t1 deep-search SSE places EVERY citable reference in
// `docs[]` (each entry carries a 1-based `index` that aligns with the
// answer's `[N]` markers), and the legacy `citations` field is now
// always `null`. Promote `docs` into both buckets — the citations
// bucket preserves array order so `citations[index - 1]` matches the
// marker, while the search-results bucket keeps the existing
// `dom_reference_links` semantics for downstream consumers.
collectSourceBucket(value.docs, summary.citations)
collectSourceBucket(value.docs, summary.searchResults)
collectSourceBucket(value.citations, summary.citations)
break
@@ -1518,8 +1543,18 @@ const yuanbaoQueryInPage = async (parameters: {
const seen = new Set<HTMLElement>()
const candidates: Array<{ element: HTMLElement; score: number }> = []
// The new yuanbao toolbar renders the deep-think toggle as a plain <div>
// with the business attribute `dt-button-id="deep_think"` (and parallel
// `dt-button-id` / `data-button-id` markers on other toolbar buttons),
// not a <button>/[role=button]. Include those selectors so we still
// match the toggle by visible text. Business attributes are far more
// stable than CSS-module-hashed class names.
for (const scope of scopes) {
const nodes = Array.from(scope.querySelectorAll("button, [role='button'], label, [tabindex]"))
const nodes = Array.from(
scope.querySelectorAll(
"button, [role='button'], label, [tabindex], [dt-button-id], [data-button-id]",
),
)
for (const node of nodes) {
if (!(node instanceof HTMLElement) || seen.has(node) || !isVisible(node)) {
continue
@@ -1912,8 +1947,8 @@ const yuanbaoQueryInPage = async (parameters: {
.split(/[,\s]+/)
.map((part) => Number.parseInt(part, 10))
.filter((index) => Number.isFinite(index) && index > 0)
.map((index) => `[citation:${index}]`)
.join(' ')
.map((index) => `[${index}]`)
.join('')
if (!markers) {
continue
}
@@ -2359,7 +2394,7 @@ export const yuanbaoAdapter: MonitorAdapter = {
dedupeSourceItems([...domCitations, ...parsed.citations, ...parsed.searchResults]),
]),
)
const answer = answerWithMarkers
const answer = normalizeCitationMarkers(answerWithMarkers)
const citations =
domCitations.length >= maxCitationMarkerIndex
? appendUniqueSourceItems(domCitations, [...parsed.citations, ...markerCitations])
@@ -2485,6 +2520,7 @@ export const __yuanbaoTestUtils = {
extractQuestionText,
findUnresolvedCitationIndexes,
mergeText,
normalizeCitationMarkers,
normalizeSourceUrl,
parseYuanbaoCaptures,
resolveCitationsFromMarkers,