Skip to content

Commit 9e5be9d

Browse files
author
SqlRush
committed
Render visible web form controls
1 parent 7964bb8 commit 9e5be9d

5 files changed

Lines changed: 93 additions & 0 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,8 @@ M7 补充:prompt history `LogEntry` 读取现在接受 `sessionID`/`session`/`
831831

832832
本轮补充:`WebFetch` HTML-to-text rendering 现在会保留 anchor `href` 作为链接上下文,并把 `img``alt`/`title`/`aria-label``src` 渲染成可见图片说明;prompt-focused excerpt 可以命中图片说明文本,同时避免重复 URL 链接文本和 `javascript:` href。
833833

834+
本轮补充:`WebFetch` HTML-to-text rendering 现在会保留可见 `input` 表单控件文本,包括 placeholder、value、alt 和 accessibility label;hidden/password/file value 会跳过,避免把非可见 secret 当页面文本暴露。
835+
834836
本轮补充:`WebFetch` GET 会记录 redirect 后的 `final_url`,HTML rendering 会按 final URL 解析相对 anchor/image URL,确保重定向页面中的相对链接和图片说明指向浏览器实际可见的目标地址。
835837

836838
本轮补充:`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`

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ test/parity/ # golden tests against TS/official behavior
193193
- 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 仍需继续补。
194194
- 本轮补充:WebFetch HEAD preflight 现在记录 `Content-Disposition`,并会通过 attachment filename 的常见二进制扩展名(如 PDF/image/archive/office/media)跳过 GET,覆盖服务端缺失 `Content-Type` 但通过下载文件名暴露类型的二进制响应。
195195
- 本轮补充: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:` 图片占位。
196+
- 本轮补充:WebFetch HTML-to-text rendering 现在会保留可见 `input` 表单控件文本,包括 placeholder、value、alt 和 accessibility label;hidden/password/file value 会跳过,避免把非可见 secret 当页面文本暴露。
196197
- 本轮补充:WebFetch GET 会记录 redirect 后的 `final_url`,HTML rendering 会按 final URL 或文档首个有效 `<base href>` 解析相对 anchor/image URL,确保重定向页面中的相对链接和图片说明指向浏览器实际可见的目标地址。
197198
- 本轮补充:WebFetch 现在按官方 cross-host redirect 语义处理跨 host 跳转,HEAD preflight 和 GET 都不会自动触达新 host,而是返回包含 original URL、redirect URL 和 status 的 redirect notice;同 host redirect 仍继续跟随并保留 `final_url`
198199
- 本轮补充:WebFetch input schema 现在与官方对齐,`url``prompt` 都是必填字段;既有本地扩展 `timeout``max_bytes`/`maxBytes` 仍保持可选。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ Covered behavior:
145145
- `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.
146146
- `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.
147147
- `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, unsafe `javascript:`/`data:`/`blob:`/`vbscript:` hrefs, and `data:` image placeholders.
148+
- `WebFetch` HTML-to-text rendering now preserves visible `input` form-control text from placeholders, values, alt text, and accessibility labels for searchable/button/image controls while skipping hidden, password, and file values so prompt-focused excerpts can match form actions without leaking non-visible secrets.
148149
- `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.
149150
- `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`.
150151
- `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 and common `/url`/`redirect`/`out` result 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: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,9 @@ func stripHTMLWebFetchTags(body string, baseURL string) string {
570570
if tag == "img" && !closing {
571571
appendHTMLWebFetchImageText(&b, rawTag, baseURL, pictureSource)
572572
}
573+
if tag == "input" && !closing {
574+
appendHTMLWebFetchInputText(&b, rawTag)
575+
}
573576
if tag == "br" || isBlockHTMLWebFetchTag(tag) {
574577
b.WriteByte('\n')
575578
}
@@ -680,6 +683,34 @@ func appendHTMLWebFetchImageText(b *strings.Builder, rawTag string, baseURL stri
680683
b.WriteByte('\n')
681684
}
682685

686+
func appendHTMLWebFetchInputText(b *strings.Builder, rawTag string) {
687+
inputType := strings.ToLower(strings.TrimSpace(htmlWebFetchAttr(rawTag, "type")))
688+
if inputType == "" {
689+
inputType = "text"
690+
}
691+
switch inputType {
692+
case "hidden", "password", "file":
693+
return
694+
}
695+
label := ""
696+
switch inputType {
697+
case "button", "submit", "reset":
698+
label = firstNonEmptyWebFetchAttr(rawTag, "value", "aria-label", "title")
699+
case "image":
700+
label = firstNonEmptyWebFetchAttr(rawTag, "alt", "aria-label", "title", "value")
701+
case "checkbox", "radio":
702+
label = firstNonEmptyWebFetchAttr(rawTag, "aria-label", "title")
703+
default:
704+
label = firstNonEmptyWebFetchAttr(rawTag, "value", "placeholder", "aria-label", "title")
705+
}
706+
if label == "" {
707+
return
708+
}
709+
b.WriteString("\nInput: ")
710+
b.WriteString(label)
711+
b.WriteByte('\n')
712+
}
713+
683714
func webFetchImageSources(rawTag string) []string {
684715
var sources []string
685716
for _, name := range []string{"srcset", "data-srcset", "data-lazy-srcset"} {

internal/tools/web/web_fetch_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,64 @@ func TestWebFetchHTMLRenderingPreservesLinksAndImageText(t *testing.T) {
303303
}
304304
}
305305

306+
func TestWebFetchHTMLRenderingPreservesVisibleFormControls(t *testing.T) {
307+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
308+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
309+
_, _ = w.Write([]byte(`<!doctype html>
310+
<html>
311+
<body>
312+
<main>
313+
<form>
314+
<label for="query">Search</label>
315+
<input id="query" type="search" placeholder="Search docs">
316+
<input type="text" value="prefilled workspace">
317+
<input type="submit" value="Run search">
318+
<input type="image" alt="Search icon" src="/assets/search.png">
319+
<input type="checkbox" aria-label="Include archived docs">
320+
<input type="hidden" value="csrf-secret">
321+
<input type="password" value="super-secret-password">
322+
<input type="file" value="/private/report.pdf">
323+
</form>
324+
</main>
325+
</body>
326+
</html>`))
327+
}))
328+
defer server.Close()
329+
executor := webExecutor(t)
330+
result, err := executor.Execute(tool.Context{Context: context.Background(), Metadata: map[string]any{}}, contracts.ToolUse{
331+
ID: "toolu_web_html_forms",
332+
Name: "WebFetch",
333+
Input: json.RawMessage(`{"url":` + strconvQuote(server.URL) + `,"prompt":"run search"}`),
334+
}, nil)
335+
if err != nil {
336+
t.Fatal(err)
337+
}
338+
rendered, ok := result.StructuredContent["rendered_body"].(string)
339+
if !ok {
340+
t.Fatalf("rendered body = %#v", result.StructuredContent["rendered_body"])
341+
}
342+
for _, want := range []string{
343+
"Input: Search docs",
344+
"Input: prefilled workspace",
345+
"Input: Run search",
346+
"Input: Search icon",
347+
"Input: Include archived docs",
348+
} {
349+
if !strings.Contains(rendered, want) {
350+
t.Fatalf("rendered body missing %q: %#v", want, rendered)
351+
}
352+
}
353+
for _, leaked := range []string{"csrf-secret", "super-secret-password", "/private/report.pdf"} {
354+
if strings.Contains(rendered, leaked) {
355+
t.Fatalf("rendered body leaked %q: %#v", leaked, rendered)
356+
}
357+
}
358+
excerpt, ok := result.StructuredContent["prompt_excerpt"].(string)
359+
if !ok || !strings.Contains(excerpt, "Input: Run search") {
360+
t.Fatalf("prompt excerpt = %#v", result.StructuredContent["prompt_excerpt"])
361+
}
362+
}
363+
306364
func TestWebFetchResolvesHTMLLinksAgainstFinalURL(t *testing.T) {
307365
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
308366
switch r.URL.Path {

0 commit comments

Comments
 (0)