Skip to content

Commit fbae84b

Browse files
author
SqlRush
committed
Skip WebFetch data image placeholders
1 parent cad3133 commit fbae84b

4 files changed

Lines changed: 66 additions & 22 deletions

File tree

docs/claude-code-go-rewrite-plan.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ test/parity/ # golden tests against TS/official behavior
179179
- WebSearch HTML/JSON 搜索适配初版已落地,覆盖 query/max_results/timeout/domain filters 输入校验、可注入搜索 endpoint、DuckDuckGo HTML 链接解析、HTML JSON-LD ItemList 结果抽取、DuckDuckGo subdomain redirect unwrap、常见 JSON result shapes、DuckDuckGo result snippet 抽取、domain allow/block 过滤、结构化结果和 query 权限规则匹配;官方搜索后端、ranking parity 和 golden 仍需继续补。
180180
- PowerShell 初版已落地,覆盖 command/timeout/description/run_in_background 输入校验、`pwsh`/`powershell` 前台执行、后台启动、`PowerShellOutput` 输出读取、`KillPowerShell` 取消、stdout/stderr/exit code/timeout/cancel 结构化结果、动态 read-only/concurrency-safe/destructive 分类、常见 mutating alias canonicalization、文件读取类命令的基础相对路径 guard、path-free `git`/`git.exe`/`git.cmd` 等外部 Git 命令复用 Bash Git safety 分类、Docker `ps`/`images`/`logs`/`inspect` 只读外部命令分类和变量/未知 flag guard、只读 PowerShell cmdlet safe-flag allowlist 和路径参数 guard、数据转换/对象检查/系统信息类 cmdlet 只读 allowlist、pipeline-tail 格式化/对象选择类 cmdlet 只读 allowlist 和变量/hashtable/scriptblock guard、网络/事件/CIM 元数据类 cmdlet 只读 allowlist 和远程/XML/hashtable 风险参数排除、native/external 原生命令只读 allowlist(`ipconfig`/`netstat`/`systeminfo`/`tasklist`/`where.exe`/`hostname`/`whoami`/`route print`/`file`/`findstr`/`dotnet` 等)和写操作形态拒绝、前台/后台输出 tool-result 截断/落盘测试覆盖、缺失可执行文件结构化错误、默认工具注册和基本跨平台进程配置;完整 parser、完整权限/path validation、后台生命周期 edge cases、官方前台截断 golden、session 记录和官方 golden 仍需继续补。
181181
- 本轮补充:WebFetch HEAD preflight 现在记录 `Content-Disposition`,并会通过 attachment filename 的常见二进制扩展名(如 PDF/image/archive/office/media)跳过 GET,覆盖服务端缺失 `Content-Type` 但通过下载文件名暴露类型的二进制响应。
182-
- 本轮补充:WebFetch HTML-to-text rendering 现在会保留 anchor `href` 作为链接上下文,并把 `img``alt`/`title`/`aria-label``src`首个 `img srcset` 候选、常见 lazy image `data-src`/`data-srcset` 候选或 `<picture><source srcset>` 候选渲染成可见图片说明;prompt-focused excerpt 可以命中图片说明文本,同时避免重复 URL 链接文本和 `javascript:` href。
182+
- 本轮补充:WebFetch HTML-to-text rendering 现在会保留 anchor `href` 作为链接上下文,并把 `img``alt`/`title`/`aria-label``src`首个可用 `img srcset` 候选、常见 lazy image `data-src`/`data-srcset` 候选或 `<picture><source srcset>` 候选渲染成可见图片说明;prompt-focused excerpt 可以命中图片说明文本,同时避免重复 URL 链接文本和 `javascript:` href`data:` 图片占位
183183
- 本轮补充:WebFetch GET 会记录 redirect 后的 `final_url`,HTML rendering 会按 final URL 或文档首个有效 `<base href>` 解析相对 anchor/image URL,确保重定向页面中的相对链接和图片说明指向浏览器实际可见的目标地址。
184184
- 本轮补充:WebFetch 现在按官方 cross-host redirect 语义处理跨 host 跳转,HEAD preflight 和 GET 都不会自动触达新 host,而是返回包含 original URL、redirect URL 和 status 的 redirect notice;同 host redirect 仍继续跟随并保留 `final_url`
185185
- 本轮补充:WebFetch input schema 现在与官方对齐,`url``prompt` 都是必填字段;既有本地扩展 `timeout``max_bytes`/`maxBytes` 仍保持可选。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ Covered behavior:
122122
- `Bash` initial shell execution, including command/timeout/description validation, `/bin/sh -c` execution, stdout/stderr/exit-code/timeout structured results, dynamic read-only/concurrency-safe/destructive classification, Git diff/log/show/status/ls-files/grep/rev-parse/branch/tag/ls-remote safe-flag validation, Git remote/push/reflog/stash/worktree/merge-base/describe/cat-file/for-each-ref/rev-list/blame/shortlog/config-get argument-aware safety classification, conservative read-only `sed` and `awk` classification for safe print/delete/quit/line-number or field-print scripts over relative paths, safe relative-path `cut`/`uniq` reads, simple safe `sort` reads, destructive `find -delete/-exec rm` and `xargs rm` classification, safe-wrapper/env prefix normalization for Bash read-only/destructive classification (`time`/`nohup`/`timeout`/`nice`/`stdbuf`/`env`), destructive command detection after temporary environment assignments, permission-rule matching through the existing tool permission adapter, background command start, same-session `BashOutput` reads, and `KillBash` cancellation.
123123
- `TodoWrite` initial todo state, including full-list writes, status/priority validation, duplicate-id rejection, one `in_progress` guard, structured result payloads, tool metadata state storage, and session-scoped local persistence/restore.
124124
- `WebFetch` initial URL fetch behavior, including URL/timeout/max-byte validation, HTTP GET, HEAD preflight, metadata/raw `skipWebFetchPreflight` skip-preflight, binary preflight GET skipping from content type or attachment filename, text/binary detection, truncation, non-2xx error marking, structured result payloads, HTML-to-text rendering, prompt-focused excerpts, prompt phrase scoring/metadata, and `WebFetch(domain:...)` permission-rule adaptation.
125-
- `WebFetch` HTML-to-text rendering now preserves anchor `href` values as link context and emits visible image text from `img` `alt`/`title`/`aria-label` plus `src`, the first `img srcset` candidate, common lazy image `data-src`/`data-srcset` candidates, or a `<picture><source srcset>` candidate, allowing prompt-focused excerpts to match image descriptions while avoiding duplicate URL link text and `javascript:` hrefs.
125+
- `WebFetch` HTML-to-text rendering now preserves anchor `href` values as link context and emits visible image text from `img` `alt`/`title`/`aria-label` plus `src`, the first usable `img srcset` candidate, common lazy image `data-src`/`data-srcset` candidates, or a `<picture><source srcset>` candidate, allowing prompt-focused excerpts to match image descriptions while avoiding duplicate URL link text and `javascript:` hrefs plus `data:` image placeholders.
126126
- `WebFetch` GET records the redirect-resolved `final_url`, and HTML rendering resolves relative anchor/image URLs against that final URL or the document's first valid `<base href>` so redirected pages expose browser-visible link and image targets.
127127
- `WebFetch` text bodies now decode common web charsets from BOM, `Content-Type`, or HTML `<meta charset>`/`http-equiv` declarations, including UTF-8/UTF-16LE/UTF-16BE, Latin-1, and Windows-1252, and structured results expose the normalized `charset`.
128128
- `WebSearch` initial HTML/JSON-search adapter, including query/max-result/timeout/domain-filter validation, injectable search endpoint, DuckDuckGo HTML link parsing, HTML JSON-LD ItemList extraction, DuckDuckGo subdomain redirect unwrapping, common JSON result shapes plus nested backend wrappers such as `web`/`response`/`hits`/`documents`, DuckDuckGo result snippet extraction, domain allow/block filtering, structured result payloads, and query-based permission-rule matching.

internal/tools/web/web_fetch.go

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,7 @@ func stripHTMLWebFetchTags(body string, baseURL string) string {
563563
}
564564
}
565565
if tag == "source" && !closing && pictureDepth > 0 && pictureSource == "" {
566-
if candidate := firstWebFetchSrcsetURL(htmlWebFetchAttr(rawTag, "srcset")); resolveWebFetchHTMLURL(candidate, baseURL) != "" {
566+
if candidate := resolveFirstWebFetchMediaURL(baseURL, webFetchSrcsetURLs(htmlWebFetchAttr(rawTag, "srcset"))...); candidate != "" {
567567
pictureSource = candidate
568568
}
569569
}
@@ -664,7 +664,9 @@ func appendHTMLWebFetchAnchorHref(b *strings.Builder, anchors []htmlWebFetchAnch
664664

665665
func appendHTMLWebFetchImageText(b *strings.Builder, rawTag string, baseURL string, sourceOverride string) {
666666
label := firstNonEmptyWebFetchAttr(rawTag, "alt", "title", "aria-label")
667-
src := resolveFirstWebFetchHTMLURL(baseURL, sourceOverride, webFetchImageSource(rawTag))
667+
srcValues := []string{sourceOverride}
668+
srcValues = append(srcValues, webFetchImageSources(rawTag)...)
669+
src := resolveFirstWebFetchMediaURL(baseURL, srcValues...)
668670
if label == "" {
669671
return
670672
}
@@ -678,47 +680,77 @@ func appendHTMLWebFetchImageText(b *strings.Builder, rawTag string, baseURL stri
678680
b.WriteByte('\n')
679681
}
680682

681-
func webFetchImageSource(rawTag string) string {
683+
func webFetchImageSources(rawTag string) []string {
684+
var sources []string
682685
for _, name := range []string{"srcset", "data-srcset", "data-lazy-srcset"} {
683-
if srcset := firstWebFetchSrcsetURL(htmlWebFetchAttr(rawTag, name)); srcset != "" {
684-
return srcset
685-
}
686+
sources = append(sources, webFetchSrcsetURLs(htmlWebFetchAttr(rawTag, name))...)
686687
}
687-
for _, name := range []string{"src", "data-src", "data-original", "data-lazy-src", "data-url"} {
688+
for _, name := range []string{"src", "data-src", "data-original", "data-original-src", "data-lazy-src", "data-url", "data-image", "data-image-src"} {
688689
if src := strings.TrimSpace(htmlWebFetchAttr(rawTag, name)); src != "" {
689-
return src
690+
sources = append(sources, src)
690691
}
691692
}
692-
return ""
693+
return sources
693694
}
694695

695-
func firstWebFetchSrcsetURL(raw string) string {
696+
func webFetchSrcsetURLs(raw string) []string {
696697
raw = strings.TrimSpace(raw)
697698
if raw == "" {
698-
return ""
699+
return nil
699700
}
701+
var urls []string
702+
skipDataContinuation := false
700703
for _, candidate := range strings.Split(raw, ",") {
701704
fields := strings.Fields(strings.TrimSpace(candidate))
702-
if len(fields) > 0 {
703-
return fields[0]
705+
if len(fields) == 0 {
706+
continue
707+
}
708+
src := strings.TrimSpace(fields[0])
709+
lower := strings.ToLower(src)
710+
if strings.HasPrefix(lower, "data:") {
711+
skipDataContinuation = true
712+
continue
713+
}
714+
if skipDataContinuation {
715+
if !looksLikeWebFetchURLCandidate(src) {
716+
continue
717+
}
718+
skipDataContinuation = false
704719
}
720+
urls = append(urls, src)
705721
}
706-
return ""
722+
return urls
707723
}
708724

709-
func resolveFirstWebFetchHTMLURL(baseURL string, rawValues ...string) string {
725+
func resolveFirstWebFetchMediaURL(baseURL string, rawValues ...string) string {
710726
for _, raw := range rawValues {
711-
raw = strings.TrimSpace(raw)
712-
if raw == "" {
727+
resolved := resolveWebFetchHTMLURL(raw, baseURL)
728+
if resolved == "" {
729+
continue
730+
}
731+
parsed, err := url.Parse(resolved)
732+
if err != nil || !parsed.IsAbs() {
713733
continue
714734
}
715-
if resolved := resolveWebFetchHTMLURL(raw, baseURL); resolved != "" {
716-
return resolved
735+
if parsed.Scheme == "http" || parsed.Scheme == "https" {
736+
return parsed.String()
717737
}
718738
}
719739
return ""
720740
}
721741

742+
func looksLikeWebFetchURLCandidate(raw string) bool {
743+
raw = strings.TrimSpace(raw)
744+
lower := strings.ToLower(raw)
745+
return strings.HasPrefix(lower, "http://") ||
746+
strings.HasPrefix(lower, "https://") ||
747+
strings.HasPrefix(raw, "//") ||
748+
strings.HasPrefix(raw, "/") ||
749+
strings.HasPrefix(raw, "./") ||
750+
strings.HasPrefix(raw, "../") ||
751+
strings.Contains(raw, ".")
752+
}
753+
722754
func firstNonEmptyWebFetchAttr(rawTag string, names ...string) string {
723755
for _, name := range names {
724756
value := strings.TrimSpace(htmlWebFetchAttr(rawTag, name))

internal/tools/web/web_fetch_test.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,8 @@ func TestWebFetchHTMLRenderingPreservesLinksAndImageText(t *testing.T) {
222222
<img aria-label="Responsive diagram" srcset="/assets/responsive-1x.png 1x, /assets/responsive-2x.png 2x">
223223
<img alt="Lazy responsive diagram" data-srcset="/assets/lazy-1x.png 1x, /assets/lazy-2x.png 2x">
224224
<img alt="Lazy source diagram" data-src="/assets/lazy-source.png">
225+
<img alt="Lazy data placeholder" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP" data-original-src="/assets/real-lazy.png">
226+
<img alt="Srcset data placeholder" srcset="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP 1x, /assets/srcset-real.png 2x">
225227
<picture>
226228
<source media="(min-width: 800px)" srcset="/assets/hero-large.png 1x, /assets/hero-large@2x.png 2x">
227229
<img alt="Hero diagram" src="/assets/hero-fallback.png">
@@ -235,6 +237,10 @@ func TestWebFetchHTMLRenderingPreservesLinksAndImageText(t *testing.T) {
235237
<source srcset="javascript:alert(3)">
236238
<img alt="Fallback diagram" src="/assets/fallback.png">
237239
</picture>
240+
<picture>
241+
<source srcset="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP 1x, /assets/picture-real.png 2x">
242+
<img alt="Picture data placeholder" src="/assets/picture-fallback.png">
243+
</picture>
238244
<a href="javascript:alert(1)">ignored script link</a>
239245
</main>
240246
</body>
@@ -269,6 +275,9 @@ func TestWebFetchHTMLRenderingPreservesLinksAndImageText(t *testing.T) {
269275
if !strings.Contains(rendered, "Image: Lazy responsive diagram ("+server.URL+"/assets/lazy-1x.png)") || !strings.Contains(rendered, "Image: Lazy source diagram ("+server.URL+"/assets/lazy-source.png)") {
270276
t.Fatalf("rendered body missing lazy image text: %#v", rendered)
271277
}
278+
if !strings.Contains(rendered, "Image: Lazy data placeholder ("+server.URL+"/assets/real-lazy.png)") || !strings.Contains(rendered, "Image: Srcset data placeholder ("+server.URL+"/assets/srcset-real.png)") {
279+
t.Fatalf("rendered body missing data-placeholder image text: %#v", rendered)
280+
}
272281
if !strings.Contains(rendered, "Image: Hero diagram ("+server.URL+"/assets/hero-large.png)") {
273282
t.Fatalf("rendered body missing picture source image text: %#v", rendered)
274283
}
@@ -278,7 +287,10 @@ func TestWebFetchHTMLRenderingPreservesLinksAndImageText(t *testing.T) {
278287
if !strings.Contains(rendered, "Image: Fallback diagram ("+server.URL+"/assets/fallback.png)") {
279288
t.Fatalf("rendered body missing unsafe picture fallback image text: %#v", rendered)
280289
}
281-
if strings.Contains(rendered, "javascript:alert") {
290+
if !strings.Contains(rendered, "Image: Picture data placeholder ("+server.URL+"/assets/picture-real.png)") {
291+
t.Fatalf("rendered body missing data-placeholder picture source text: %#v", rendered)
292+
}
293+
if strings.Contains(rendered, "javascript:alert") || strings.Contains(rendered, "data:image") || strings.Contains(rendered, "R0lGOD") {
282294
t.Fatalf("rendered body kept unsafe href: %#v", rendered)
283295
}
284296
excerpt, ok := result.StructuredContent["prompt_excerpt"].(string)

0 commit comments

Comments
 (0)