Files
dobotai-platform/internal/channels/dingtalk/card_diagnostics.go
T
姚章浩 cfe3271fa2 feat(dingtalk): fall back to the public image URL and report image ref shape
The loopback fetch now degrades to the public URL ...
2026-09-17 18:20:02 +08:00

120 lines
3.5 KiB
Go

package dingtalk
import (
"fmt"
"strings"
"gitea.zheromiku.top/Weknora/dobotai-platform/internal/reasoningmode"
)
const reasoningBannerMarker = "【智能推理模式】"
type cardContentShape struct {
bytes int
lines int
pipeLines int
leadingPipeLines int
delimiterLines int
confirmedTable bool
confirmedTablePos int
// Structural split state: where the answer body starts inside this exact
// payload. bodyPrefixRunes > 0 means the update still carries narration in
// front of the body (the reasoning text the card is supposed to hide).
bodyOK bool
bodyLine int
bodyPrefixRunes int
bannerPos int
firstHeadingLine int
imageRefs int
externalImageRefs int
mediaImageRefs int
}
func inspectCardContent(content string) cardContentShape {
shape := cardContentShape{
bytes: len(content),
confirmedTablePos: -1,
bodyLine: -1,
firstHeadingLine: -1,
bannerPos: -1,
}
if content == "" {
return shape
}
shape.lines = strings.Count(content, "\n") + 1
shape.imageRefs, shape.externalImageRefs, shape.mediaImageRefs = cardImageRefCounts(content)
shape.bannerPos = strings.Index(content, reasoningBannerMarker)
// The streaming path decorates every frame with the reasoning banner, so the
// prefix in front of the body has to be measured after that banner: only
// what remains is narration the card is meant to hide.
bannerSkip := 0
if shape.bannerPos == 0 {
if idx := strings.Index(content, "\n"); idx >= 0 {
bannerSkip = idx + 1
for bannerSkip < len(content) && content[bannerSkip] == '\n' {
bannerSkip++
}
}
}
if offset, ok := reasoningmode.BodyStartOffset(content); ok {
shape.bodyOK = true
shape.bodyLine = strings.Count(content[:offset], "\n")
prefix := content[:offset]
if bannerSkip > 0 && bannerSkip <= len(prefix) {
prefix = prefix[bannerSkip:]
}
shape.bodyPrefixRunes = len([]rune(prefix))
}
lines := splitMarkdownStreamLines(content)
for i, line := range lines {
trimmed := strings.TrimSpace(line.text)
if shape.firstHeadingLine < 0 && !line.inFence &&
strings.HasPrefix(trimmed, "#") && strings.TrimLeft(trimmed, "#") != "" {
shape.firstHeadingLine = i
}
if strings.Contains(line.text, "|") {
shape.pipeLines++
}
if isTableHeaderCandidate(line.text) {
shape.leadingPipeLines++
}
if !line.inFence && isCompleteTableDelimiter(line.text) {
shape.delimiterLines++
}
if shape.confirmedTable || i == 0 || line.inFence || lines[i-1].inFence {
continue
}
if isTableHeaderCandidate(lines[i-1].text) && isCompleteTableDelimiter(line.text) {
shape.confirmedTable = true
shape.confirmedTablePos = lines[i-1].start
}
}
return shape
}
func formatCardUpdateDiagnostic(content string, isFinalize, isError bool) string {
shape := inspectCardContent(content)
return fmt.Sprintf(
"DingTalk card update shape: finalize=%t error=%t bytes=%d lines=%d pipe_lines=%d leading_pipe_lines=%d delimiter_lines=%d confirmed_table=%t table_start=%d body_ok=%t body_line=%d body_prefix_runes=%d banner_pos=%d first_heading_line=%d image_refs=%d external_image_refs=%d media_image_refs=%d",
isFinalize,
isError,
shape.bytes,
shape.lines,
shape.pipeLines,
shape.leadingPipeLines,
shape.delimiterLines,
shape.confirmedTable,
shape.confirmedTablePos,
shape.bodyOK,
shape.bodyLine,
shape.bodyPrefixRunes,
shape.bannerPos,
shape.firstHeadingLine,
shape.imageRefs,
shape.externalImageRefs,
shape.mediaImageRefs,
)
}