Skip to content

Commit d848b94

Browse files
author
SqlRush
committed
Render accessible WebFetch link labels
1 parent 189d486 commit d848b94

4 files changed

Lines changed: 55 additions & 1 deletion

File tree

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,8 @@ M5 补充:WebFetch prompt-focused excerpt 的 term scoring 现在会匹配常
183183

184184
M5 补充:WebFetch HTML-to-text rendering 现在会跳过 `hidden``aria-hidden="true"`、CSS `display:none`/`visibility:hidden` 的不可见元素子树,隐藏文本和图片说明不会进入 rendered body 或 prompt-focused excerpt。
185185

186+
M5 补充:WebFetch HTML-to-text rendering 现在会为无可见文本但带 `aria-label`/`title` 的链接保留可访问名称和 resolved href,icon-only 链接可进入 rendered body 与 prompt-focused excerpt。
187+
186188
M5 补充:WebSearch HTML snippet 提取现在识别 Bing 风格 `b_caption`/`b_snippet` 以及常见搜索摘要 class,标题 anchor 后的可见摘要会进入文本输出和 structured result。
187189

188190
M5 补充:WebSearch HTML 结果 anchor 现在会识别 `data-href`/`data-url` 等常见 data 属性中的真实结果 URL,并跳过 `href="#"`/`javascript:` 占位链接,覆盖搜索页用 JS 事件承载跳转地址的形态。

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ test/parity/ # golden tests against TS/official behavior
200200
- 本轮补充:WebFetch 文本 body 现在会按 BOM、`Content-Type` charset 或 HTML `<meta charset>`/`http-equiv` charset 解码常见网页编码,包括 UTF-8/UTF-16LE/UTF-16BE、Latin-1 和 Windows-1252,并在 structured content 暴露归一化 `charset`
201201
- 本轮补充:WebFetch prompt-focused excerpt 的 term scoring 现在会匹配常见单复数变体,例如 prompt `cost` 可命中正文 `costs`,同时仍保持词边界匹配避免子串误命中。
202202
- 本轮补充:WebFetch HTML-to-text rendering 现在会跳过 `hidden``aria-hidden="true"`、CSS `display:none`/`visibility:hidden` 的不可见元素子树,隐藏文本和图片说明不会进入 rendered body 或 prompt-focused excerpt。
203+
- 本轮补充:WebFetch HTML-to-text rendering 现在会为无可见文本但带 `aria-label`/`title` 的链接保留可访问名称和 resolved href,icon-only 链接可进入 rendered body 与 prompt-focused excerpt。
203204
- 本轮补充:WebSearch HTML 结果解析现在会按搜索页首个有效 `<base href>` 解析相对结果 anchor,覆盖镜像/自定义搜索页中浏览器可见结果 URL 与请求路径不一致的情况。
204205
- 本轮补充:WebSearch HTML 结果解析现在会读取 `application/ld+json` JSON-LD 结果,递归抽取 `@graph``ItemList.itemListElement.item`,支持 JSON-LD `@id` URL alias,并与后续 anchor 结果按 URL 去重。
205206
- 本轮补充:WebSearch HTML snippet 提取现在识别 Bing 风格 `b_caption`/`b_snippet` 以及常见搜索摘要 class,标题 anchor 后的可见摘要会进入文本输出和 structured result。

internal/tools/web/web_fetch.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,8 @@ func stripHTMLWebFetchTags(body string, baseURL string) string {
620620
anchors, _ = appendHTMLWebFetchAnchorHref(&b, anchors)
621621
} else {
622622
href := strings.TrimSpace(htmlWebFetchAttr(rawTag, "href"))
623-
anchors = append(anchors, htmlWebFetchAnchor{Href: resolveWebFetchHTMLURL(href, baseURL), Start: b.Len()})
623+
label := firstNonEmptyWebFetchAttr(rawTag, "aria-label", "title")
624+
anchors = append(anchors, htmlWebFetchAnchor{Href: resolveWebFetchHTMLURL(href, baseURL), Label: label, Start: b.Len()})
624625
}
625626
}
626627
if tag == "button" {
@@ -752,6 +753,7 @@ func resolveWebFetchBaseURL(raw string, fallback string) string {
752753

753754
type htmlWebFetchAnchor struct {
754755
Href string
756+
Label string
755757
Start int
756758
}
757759

@@ -787,6 +789,14 @@ func appendHTMLWebFetchAnchorHref(b *strings.Builder, anchors []htmlWebFetchAnch
787789
if anchor.Start >= 0 && anchor.Start <= len(current) {
788790
text = strings.Join(strings.Fields(current[anchor.Start:]), " ")
789791
}
792+
if text == "" && strings.TrimSpace(anchor.Label) != "" {
793+
if b.Len() > 0 {
794+
b.WriteByte('\n')
795+
}
796+
b.WriteString("Link: ")
797+
b.WriteString(strings.TrimSpace(anchor.Label))
798+
text = strings.TrimSpace(anchor.Label)
799+
}
790800
if text != "" && (text == href || strings.Contains(text, href)) {
791801
return anchors, false
792802
}

internal/tools/web/web_fetch_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,47 @@ func TestWebFetchHTMLRenderingSkipsHiddenElements(t *testing.T) {
296296
}
297297
}
298298

299+
func TestWebFetchHTMLRenderingPreservesAccessibleLinkLabels(t *testing.T) {
300+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
301+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
302+
_, _ = w.Write([]byte(`<!doctype html>
303+
<html>
304+
<body>
305+
<main>
306+
<a href="/reports/q4" aria-label="Download quarterly report"><svg><title>ignored download icon</title></svg></a>
307+
<a href="/help" title="Help center"></a>
308+
<a href="javascript:alert(1)" aria-label="Unsafe menu"></a>
309+
</main>
310+
</body>
311+
</html>`))
312+
}))
313+
defer server.Close()
314+
executor := webExecutor(t)
315+
result, err := executor.Execute(tool.Context{Context: context.Background(), Metadata: map[string]any{}}, contracts.ToolUse{
316+
ID: "toolu_web_html_accessible_links",
317+
Name: "WebFetch",
318+
Input: json.RawMessage(`{"url":` + strconvQuote(server.URL) + `,"prompt":"quarterly report"}`),
319+
}, nil)
320+
if err != nil {
321+
t.Fatal(err)
322+
}
323+
rendered, ok := result.StructuredContent["rendered_body"].(string)
324+
if !ok {
325+
t.Fatalf("rendered body = %#v", result.StructuredContent["rendered_body"])
326+
}
327+
if !strings.Contains(rendered, "Link: Download quarterly report ("+server.URL+"/reports/q4)") ||
328+
!strings.Contains(rendered, "Link: Help center ("+server.URL+"/help)") {
329+
t.Fatalf("rendered body missing accessible link labels: %#v", rendered)
330+
}
331+
if strings.Contains(rendered, "ignored download icon") || strings.Contains(rendered, "Unsafe menu") || strings.Contains(rendered, "javascript:alert") {
332+
t.Fatalf("rendered body leaked hidden/unsafe accessible link text: %#v", rendered)
333+
}
334+
excerpt, ok := result.StructuredContent["prompt_excerpt"].(string)
335+
if !ok || !strings.Contains(excerpt, "Download quarterly report") {
336+
t.Fatalf("prompt excerpt missing accessible link label: %#v", result.StructuredContent["prompt_excerpt"])
337+
}
338+
}
339+
299340
func TestWebFetchHTMLRenderingPreservesLinksAndImageText(t *testing.T) {
300341
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
301342
w.Header().Set("Content-Type", "text/html; charset=utf-8")

0 commit comments

Comments
 (0)