The weknora HTTP client used a hardcoded 120s total-duration timeout, which
kills any smart-reasoning agent stream that legitimately runs longer (22 tool
rounds measured in prod) and the failure branch then overwrote the already
streamed card with the error text. Pass 0 and keep Go's default (no timeout).
The DingTalk binding can point at the quick-answer agent, which publishes the
whole answer as one answer run plus tool_call/tool_result steps. The narration
boundary was rejected for that shape (start=0), so the text-only heuristic ran
and dropped the answer lead-in together with its image.
- answer_observation: a single answer run means there is no narration to cut.
- outboundtext: rewrite LaTeX formulas into plain Unicode text, because
DingTalk cards render no math typesetting (r = sqrt(X^2 + Y^2) -> r = √(X² + Y²)).
- dingtalk handler: normalize formulas on both the partial and final card paths.
Adds the production CRA5 shape (narration run + tool steps + final answer run
that opens with a lead-in and an image) and a control case where the last
answer run is interrupted by a tool call and the heuristic still applies.
The DingTalk card dropped the answer's opening (and any image reference in
it) whenever the agent narrated its steps before answering: the text-only
boundary heuristic (reasoningmode.BodyStartOffset) treats everything before
the first structural line as narration, so a lead-in like "我已经找到了…"
plus the image that follows it disappeared, leaving the card to start at
the first heading.
The event stream already knows the boundary: every agent step publishes its
answer chunks under its own answer run id (data.event_id), and the final
body is the last run that no tool/thinking event interrupts. WeKnora
StreamEvent now carries the run id and run start offset; qa.Service tracks
the observation and prefers it over the heuristic, falling back to the
heuristic when the stream carries no run information.
Verified: production turn efae317e (给我CRA5的负载图) lost 131 chars —
lead-in + the load-curve image + separator — and now keeps them.
DINGTALK_CARD_WIDE_TABLE_GROUP=off keeps over-wide answer tables as tables on the final frame (still replaces <br> inside cells) so the PC wide-screen card can lay them out itself.
- outboundimage: re-encode large PNG/JPEG screenshots as downscaled JPEG (1600px long edge, q82, transparency flattened onto white) before upload; undecodable, small, GIF/WebP payloads are published untouched
- outboundimage: StableHashPublisher skips the upload when the content-addressed object already exists (ObjectStater); OSS store implements it via IsObjectExist
- cmd/dobotai: DOBOTAI_OUTBOUND_IMAGE_REWRITE_TIMEOUT default 60s -> 180s; DOBOTAI_OUTBOUND_IMAGE_COMPRESS (on by default, off restores raw bytes)
- outboundimage: fetch and publish up to 4 images in parallel, dedupe in document order, log per-image outcome plus a rewrite summary
- dingtalk handler: OutboundRewriteTimeout applies to the image rewrite stage only, falling back to finalDeliveryTimeout
- cmd/dobotai: DOBOTAI_OUTBOUND_IMAGE_REWRITE_TIMEOUT (default 60s, invalid values fall back)
AI card bodies render only DingTalk-hosted media, so published image URLs are
rewritten into media ids inside card content (streaming frames never block,
final frame waits briefly), while the markdown fallback keeps external URLs.
WeKnora's system prompt hard-enforces 'ALWAYS respond in {{language}}',
fed by its Language middleware (WEKNORA_LANGUAGE env -> Accept-Language
header -> zh-CN fallback). The previous localizedQuery() prompt prefix
lost against that instruction, so a user-set 'en' preference still got
Chinese answers on English questions.
- AgentChatRequest.Locale is transmitted as the Accept-Language header
(never in the JSON body); WeKnora maps it to the prompt language
- 'auto' detects the query script client-side (latin->en, Han->zh-CN,
kana->ja, Hangul->ko); es is never auto-guessed
- unset/unknown locale sends no header, preserving prior server behavior
- drop localizedQuery prompt prefixes entirely