mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-06 09:17:31 +00:00
fix(webui): 聊天图片"加载成功却显示加载失败"——HTML 属性里的反斜杠转义
## 缺陷
生产截图:图片加载成功了(img 元素在、src 正确),界面却多显示一行
蓝字「图片加载失败"/>」。
根因是内联事件属性里用了反斜杠转义:
onerror="this.parentElement.innerHTML='<span class=\"att-err\">图片加载失败</span>'"
**HTML 属性值里没有反斜杠转义** —— 那个 \" 就是引号本身,属性在
`<span class=` 之后就闭合了。浏览器切出来的是:
onerror = "this.parentElement.innerHTML='<span class="
剩下的 `att-err">图片加载失败</span>'"/>` 变成**文本节点**直接显示。
症状极具误导性:看起来像"图片坏了",实际图片是好的;而那个 onerror
因为语法错误**永不执行** —— 真失败时反而什么都不显示。
## 修法
不用内联属性(那要求以后每次拼 HTML 都记得转义规则),改成
**捕获阶段的事件委托**:
- error 事件**不冒泡**,只能在捕获阶段拿到(注释里写明了)
- 用 createElement + textContent 构造占位,不再拼 innerHTML 字符串
- 只处理 .chat-attachment-img,不影响正文里的 markdown 图片
## 顺带:两个类没有 CSS
`chat-attachment-img` 与 `att-err` 只出现在 JS 里,CSS 里一条都没有。
图片靠内联 style 撑着还能看,失败占位则完全没有样式。已补。
## 判据(2 条,变异验证)
- `TestChatAttachmentImgHasNoBrokenInlineHandler`:不许内联 onerror、
必须用 DOM API、必须有 error 监听
- `TestDashboardNoEscapedQuoteInHTMLAttributes`:通用扫描,
任何 `name="...\"...` 形态都会红(下次别人写内联事件也拦得住)
变异:把内联 onerror 放回去 ⇒ 两条同时红。
★ 写判据时踩到的坑(都已修):判据被**自己的注释**打红(注释里必须写出
错误写法来解释),需先用已有的 stripJSComments 剥离;另外我先自己写了一个
stripJSComments,与 dashboard_js_scope_test.go 的重名 —— 应复用已有的。
## 顺带修一条脆弱判据
`TestRTChild_RapidInputCollapsesToLatest` 全量跑时偶发红
("连发 50 条却跑了 5 轮")。
探针查实:连发 50 条只花 ~80µs,而测试 provider 的 ChatStream **同步瞬间
返回**,两者同量级 ⇒ 消费方能在连发过程中抢到几次执行。探针显示
dropped=48,collapse 明明生效了。
所以 `Rounds > 3` 是在测调度抖动,不是测契约。改为断言**覆盖主导**
(dropped >= burst/2)。真实通话每轮几十毫秒,连发微秒级,生产里
"轮次≈1" 自然成立。
变异验证:让 dropped 不计数 ⇒ 判据红(dropped=0(连发 50 条))。
go build/vet/gofmt 干净;go test ./... -count=1 -race = 62 ok / 0 FAIL / 0 RACE
This commit is contained in:
@ -2370,6 +2370,44 @@ function renderChat() {
|
||||
},
|
||||
true,
|
||||
);
|
||||
// 图片加载失败:**必须用捕获阶段**的委托,不能写成内联 onerror。
|
||||
//
|
||||
// ## 原来坏在哪(2026-10-30 实测)
|
||||
//
|
||||
// 原代码是:
|
||||
// onerror="this.parentElement.innerHTML='<span class=\"att-err\">图片加载失败</span>'"
|
||||
//
|
||||
// HTML **属性值里没有反斜杠转义** —— 那个 \" 就是引号本身,
|
||||
// 于是 onerror 属性在 <span class= 之后就闭合了。浏览器切出来的属性是:
|
||||
// onerror = "this.parentElement.innerHTML='<span class="
|
||||
// 剩下的 `att-err">图片加载失败</span>'"/>` 变成**文本节点**直接显示。
|
||||
//
|
||||
// 症状因此极具误导性:图片明明加载成功了(img 元素在、src 正确),
|
||||
// 界面上却多出一行「图片加载失败"/>」;而那个 onerror 本身因为语法
|
||||
// 错误永远不会执行 —— 真失败时反而什么都不显示。
|
||||
//
|
||||
// ## 为什么用 error 捕获而不是重写内联
|
||||
//
|
||||
// error 事件**不冒泡**,只有在捕获阶段才能委托到容器上。
|
||||
// 改内联也能修(用单引号包属性值 + 不嵌套引号),但那仍然要求
|
||||
// 以后每次拼 HTML 都记得转义规则;委托把这条规则从"每次都要做对"
|
||||
// 变成"只在一处写对"。
|
||||
msgsEl.addEventListener(
|
||||
"error",
|
||||
(e) => {
|
||||
var t = e.target;
|
||||
if (!t || t.tagName !== "IMG" || !t.classList.contains("chat-attachment-img")) {
|
||||
return;
|
||||
}
|
||||
var box = t.parentElement;
|
||||
if (!box) return;
|
||||
var span = document.createElement("span");
|
||||
span.className = "att-err";
|
||||
span.textContent = __("图片加载失败", "Image failed to load");
|
||||
box.replaceChildren(span);
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
var msgs = state.messages;
|
||||
var sig =
|
||||
@ -2470,7 +2508,7 @@ function renderChat() {
|
||||
escHtml(att.url) +
|
||||
'" ' +
|
||||
'alt="image" loading="lazy" style="max-width:320px;max-height:240px;border-radius:10px;display:block;cursor:zoom-in" ' +
|
||||
'onerror="this.parentElement.innerHTML=\'<span class=\\"att-err\\">图片加载失败</span>\'"/></a>';
|
||||
'/></a>';
|
||||
} else if (att.type === "audio") {
|
||||
// 音频用原生播放器:与图片同理,附件能在聊天里直接消费才算可见。
|
||||
// preload="metadata" 只拉时长不拉全部字节,避免历史消息满屏时并发下载。
|
||||
|
||||
Reference in New Issue
Block a user