fix(backthink): 后台思考静默广播——不再把内部推理泄到聊天思维链 + XML 动作/旁白解析
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

【静默广播修复(bug:思维链露出 backthink 推理)】
- 根因:LoopThinker.Think 复用同一 loop.Loop 且 loop 全局带 OnStream,backthink 后台思考时
  把 [background autonomous reflection] 的 thinking/流式事件广播到前端,被当聊天思维链显示。
- loop/mute_ctx.go:WithMutedBroadcast(ctx) + isMutedBroadcast(ctx)——context 上静默标志。
- loop 四处广播抑制(stream.go 流式 / step.go 静默走非流式 agentStep / request.go capacity 事件 /
  tools.go tool_execution),静默时跳过 UI 广播,推理照常返回结论。
- thinker.go LoopThinker.Think 用 loop.WithMutedBroadcast(ctx),backthink 不再泄 thinking。

【XML 动作/旁白结构化解析】
- cyrene.yaml systemPrompt 加「表达方式:动作与旁白」——动作 <action>…</action>、
  旁白 <narration>…</narration>;克制≤1-2个/段、贴合唤醒态、重要信息不加、朗读正文忽略标签。
- 前端 parseContent.ts:ContentPart[](text/action/narration),非贪婪+反向引用、跨行、非法/
  未闭合兜底纯文本不崩;summarizeContent 供思维导图/状态。
- 前端 renderContent.tsx:action/narration → 灰斜体(mu-text-lo italic)句中原位渲染。
- 应用到 chat.tsx UserBubble/AssistantBubble + messages 各 mode body。

go build/vet/test(loop/backthink)绿 + 前端 tsc/vite 绿。
This commit is contained in:
Aska committed 2026-09-06 22:09:51 +08:00
1 parent 6dbb4a2e18
commit 33eeadfc38
11 files changed
+146 -19

No files matched your search

+3 -2
View File
@@ -4,6 +4,7 @@ import { Tag } from "./primitives";
import { Icon, type IconName } from "./Icon";
import { WakePath } from "./status";
import { modeBody } from "./messages";
import { renderContent } from "../lib/renderContent";
/** Collapsible inline chain-of-thought block. */
export function ThinkingBlock({
@@ -96,9 +97,9 @@ export function MessageBubble({
function UserBubble({ message, mode }: { message: ChatMessage; mode: Mode }): React.ReactNode {
const body: React.ReactNode =
mode === "code" ? (
<pre className="whitespace-pre-wrap font-mono text-xs leading-relaxed">{message.content}</pre>
<pre className="whitespace-pre-wrap font-mono text-xs leading-relaxed">{renderContent(message.content)}</pre>
) : message.content ? (
<span className="whitespace-pre-wrap">{message.content}</span>
<span className="whitespace-pre-wrap">{renderContent(message.content)}</span>
) : null;
return (
<div className="mu-rise-in flex justify-end">
@@ -4,6 +4,7 @@ import type { ChatMessage, Mode } from "../../lib/types";
import { Tag } from "../primitives";
import { Icon } from "../Icon";
import { useAppStore } from "../../store/useAppStore";
import { renderContent } from "../../lib/renderContent";
/**
* Mode-specific assistant message bodies (design/07 §2). The shared outer
@@ -22,7 +23,7 @@ function shell(children: ReactNode): ReactNode {
function fallbackText(message: ChatMessage): ReactNode {
return message.content ? (
<span className="whitespace-pre-wrap">{message.content}</span>
<span className="whitespace-pre-wrap">{renderContent(message.content)}</span>
) : message.streaming ? (
<span className="mu-blink text-[var(--mu-text-lo)]">生成中…</span>
) : null;
@@ -34,7 +35,7 @@ export function CodeMessageBody({ message }: { message: ChatMessage }): ReactNod
<div className="space-y-2">
{message.content ? (
<pre className="overflow-x-auto whitespace-pre-wrap rounded-[var(--mu-radius-sm)] bg-[var(--mu-surface-2)] p-3 font-mono text-xs leading-relaxed text-[var(--mu-text-mid)]">
{message.content}
{renderContent(message.content)}
</pre>
) : message.streaming ? (
<div className="mu-blink text-xs text-[var(--mu-text-lo)]">正在执行 / 生成中…</div>
@@ -51,7 +52,7 @@ export function DocsMessageBody({ message }: { message: ChatMessage }): ReactNod
<div className="flex items-start gap-2">
<Icon name="docs" size={18} className="mt-0.5 shrink-0 text-[var(--mu-accent)]" />
<div className="min-w-0 space-y-1">
<div className="text-sm font-medium">{message.content}</div>
<div className="text-sm font-medium">{renderContent(message.content)}</div>
<Tag tone="neutral">知识库</Tag>
</div>
</div>
@@ -68,7 +69,7 @@ export function HomeMessageBody({ message }: { message: ChatMessage }): ReactNod
return (
<div className="space-y-2">
{message.content ? (
<span className="whitespace-pre-wrap">{message.content}</span>
<span className="whitespace-pre-wrap">{renderContent(message.content)}</span>
) : null}
{devices.length > 0 ? (
<div className="flex flex-wrap gap-2 pt-1">
@@ -97,7 +98,7 @@ export function WorkMessageBody({ message }: { message: ChatMessage }): ReactNod
{tasks.map((t, i) => (
<div key={i} className="flex items-center gap-2 text-sm">
<Icon name="check" size={14} className="shrink-0 text-[var(--mu-ok)]" />
<span className="min-w-0 whitespace-pre-wrap">{t}</span>
<span className="min-w-0 whitespace-pre-wrap">{renderContent(t)}</span>
</div>
))}
</div>
@@ -130,7 +131,7 @@ export function VisionMessageBody({ message }: { message: ChatMessage }): ReactN
{hasImage ? <Tag tone="accent">识别</Tag> : null}
</div>
{message.content ? (
<span className="whitespace-pre-wrap text-sm text-[var(--mu-text-hi)]">{message.content}</span>
<span className="whitespace-pre-wrap text-sm text-[var(--mu-text-hi)]">{renderContent(message.content)}</span>
) : message.streaming ? (
<span className="mu-blink text-xs text-[var(--mu-text-lo)]">识别中…</span>
) : null}
@@ -201,7 +202,7 @@ export function VoiceMessageBody({ message }: { message: ChatMessage }): ReactNo
</div>
{message.content ? (
<div className="mt-2 whitespace-pre-wrap text-xs text-[var(--mu-text-lo)]">{message.content}</div>
<div className="mt-2 whitespace-pre-wrap text-xs text-[var(--mu-text-lo)]">{renderContent(message.content)}</div>
) : null}
</div>
);
@@ -209,7 +210,7 @@ export function VoiceMessageBody({ message }: { message: ChatMessage }): ReactNo
/** agents — sub-agent progress card (kept for the shared flow; monitors live in messages). */
export function AgentsMessageBody({ message }: { message: ChatMessage }): ReactNode {
return shell(<span className="whitespace-pre-wrap">{message.content || fallbackText(message)}</span>);
return shell(<span className="whitespace-pre-wrap">{message.content ? renderContent(message.content) : fallbackText(message)}</span>);
}
/** Reserved — settings is managed in the admin panel; an opaque content fallback. */