Repository navigation
Description
tdesign-react 版本
- tdesign-web-components:
1.3.0-alpha.2(项目在用)、1.3.1-alpha.14(最新 alpha,同样存在) - @tdesign/ai-chat-engine:0.1.0(同样存在) - @tdesign-react/chat:1.0.2- 协议:agui(protocol: 'agui',流式) - Node:v24.3.0(复现脚本)/v20.19.4(业务构建) - 浏览器: Chrome 最新版 - 系统: macOS
重现链接
No response
重现步骤
No response
期望结果
No response
实际结果
No response
框架版本
No response
浏览器版本
No response
系统版本
No response
Node版本
No response
补充说明
问题描述
ChatEngine.handleComplete 在正常完成分支判断本轮是否失败时,扫描的是整个会话的所有消息,而不是当前这条消息。因此只要会话里曾经出现过任何一条失败消息(例如上一轮请求失败、或从历史记录恢复出来的失败回答),此后每一轮即使上游正常结束(RUN_FINISHED、无 RUN_ERROR 帧),本轮也会被判为 error。
进一步地,setMessageStatus(id, 'error') 会把本轮消息最后一个内容块的 status 同步改成 error。由于末块通常就是本轮回答正文,业务侧据此落库/渲染时会把正文当作错误信息处理:正文丢失、界面显示“请求出错”。
相关源码
esm/chat-engine/index.js(lib、cjs 产物同):
// handleComplete 正常完成分支
const allContentFailed = this.messageStore.messages.find(
(content) => content.status === "error"
);
this.setMessageStatus(id, isAborted ? "stop" : allContentFailed ? "error" : "complete");store/message.js:
setMessageStatus(messageId, status) {
// ...
message.status = status;
const lastContent = message.content[message.content.length - 1];
if ("status" in lastContent && lastContent.status !== "error") {
lastContent.status = status; // 末内容块被同步改成 error
}
}变量名 allContentFailed、回调参数名 content 看起来原意是想检查本条消息的内容块,但实现变成了遍历全部消息。
重现步骤
最小复现(Node 环境,无需浏览器):
mkdir td-repro && cd td-repro
npm init -y
npm i tdesign-web-components@1.3.1-alpha.14 immerrepro.mjs:
import ChatEngine from 'tdesign-web-components/lib/chat-engine/index.js';
const engine = new ChatEngine();
engine.init(() => ({}), [
// 历史中已有一条失败回答
{
id: 'old-failed',
role: 'assistant',
status: 'error',
content: [{ type: 'text', data: '上一轮失败', status: 'error' }],
},
// 本轮:上游正常结束,正文已完整写入
{
id: 'current',
role: 'assistant',
status: 'streaming',
content: [{ type: 'text', data: '本轮正常回答正文', status: 'streaming' }],
},
]);
// RUN_FINISHED 时引擎内部调用的同一个入口
engine.handleComplete('current', false, {}, undefined);
const current = engine.messageStore.getMessageByID('current');
const lastBlock = current.content[current.content.length - 1];
console.log('current.status :', current.status);
console.log('lastBlock.status:', lastBlock.status);
console.log('lastBlock.data :', lastBlock.data);执行 node repro.mjs。
业务侧更容易触发的操作路径:
- 在同一个会话里让某一轮回答失败(断网、后端 5xx 均可),不要点“重新发送”,也不要新建会话;
- 保持会话不变,直接再问一个新问题,让上游正常返回;
- 观察本轮回答:上游日志显示成功收尾,但界面显示“请求出错”,正文消失。
刷新页面后同样能触发:历史失败回答会以 status: 'error' 恢复进引擎,之后每一轮都会被污染。若业务方在重新发送时会先移除失败回答,则不会触发,这也是该问题容易被自测漏掉的原因。
期望结果
本轮终态只取决于当前这条消息自身是否失败:current.status === 'complete'、末内容块 status === 'complete',正文保留。
实际结果
current.status === 'error'、末内容块 status === 'error'(两个版本均复现):
version=1.3.0-alpha.2 current.status=error lastBlock.status=error data=本轮正常回答正文
version=1.3.1-alpha.14 current.status=error lastBlock.status=error data=本轮正常回答正文
建议修复
在 handleComplete 里把判定范围从“全部消息”收窄到“当前消息”,同时覆盖“消息状态已标记 error”和“仅内容块被标记 error”两种情况:
const currentMessage = this.messageStore.getMessageByID(id);
const allContentFailed =
!!currentMessage &&
(currentMessage.status === "error" ||
(Array.isArray(currentMessage.content) &&
currentMessage.content.some((content) => content?.status === "error")));
this.setMessageStatus(id, isAborted ? "stop" : allContentFailed ? "error" : "complete");补充:原实现在“本轮末块已是 error、但消息状态尚未标记 error”的情况下会误判为 complete,上述写法一并修正。
补充说明
- 该问题也存在于拆分后的独立包
@tdesign/ai-chat-engine@0.1.0(handleComplete中仍是this.messageStore.messages.find((content) => content.status === "error")),一并排查会更彻底。 handleComplete目前是private,业务侧只能通过替换实例方法的 monkey patch 方式规避,升级时容易静默失效,因此希望上游修正。- 临时规避方式(业务侧):不使用
status === 'stop'作为历史失败消息的恢复状态(会渲染成“已停止”),也不要用onComplete返回{ status: 'complete' }(processMessageResult只改内容不改消息状态,会停在pending)。目前可行的是依赖补丁修改该判定,或在本轮结束后自行纠正终态。
环境信息
- tdesign-web-components:
1.3.0-alpha.2(项目在用)、1.3.1-alpha.14(最新 alpha,同样存在) - @tdesign/ai-chat-engine:
0.1.0(同样存在) - @tdesign-react/chat:
1.0.2 - 协议:
agui(protocol: 'agui',流式) - Node:
v24.3.0(复现脚本)/v20.19.4(业务构建) - 浏览器: Chrome 最新版
- 系统: macOS
Activity
github-actions commentedon Nov 12, 2026
👋 @hzkkk,感谢给 TDesign 提出了 issue。
请根据 issue 模版确保背景信息的完善,我们将调查并尽快回复你。
lnuxe commentedon Nov 12, 2026
已定位并修复:修复 PR → TDesignOteam/tdesign-ai-core#42(tdesign-react 的
packages/ai-core子模块即该仓库,源码同源)。根因(与楼主分析一致):
handleComplete的allContentFailed = messageStore.messages.find((content) => content.status === 'error')扫描的是整个会话的消息数组——历史任何一条 error 消息都会让后续每一轮正常收尾被判为error,再经setMessageStatus把本轮末内容块污染成 error。修复:结算范围收窄到当前消息——
getMessageByID(id).content.some(c => c.status === 'error')。验证(本地 vitest,修复前 → 修复后):
修复同时落实了该测试文件里原有的
it.todo('完成消息时忽略无关的更早消息上的错误状态'),新增 2 条回归测试锁定两个方向:历史失败不外溢 + 当前内容块失败仍正确判 error。