Skip to content

Commit 2f84802

Browse files
author
SqlRush
committed
Match WebFetch prompt plural variants
1 parent 51abe82 commit 2f84802

4 files changed

Lines changed: 84 additions & 2 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ M5 补充:BashOutput/PowerShellOutput structured content 现在会回传实际
179179

180180
M5 补充:Read/Edit 现在接受 `offset`/`limit``replace_all` 的 quoted semantic string 输入;whole-decimal 数字字符串如 `"2.0"` 会按官方 `semanticNumber(...int())` 语义归一为整数,fractional 数字仍会被拒绝。
181181

182+
M5 补充:WebFetch prompt-focused excerpt 的 term scoring 现在会匹配常见单复数变体,例如 prompt `cost` 可命中正文 `costs`,同时仍保持词边界匹配避免子串误命中。
183+
182184
M5 补充:`Grep` content 输出现在支持 `only_matching`/`onlyMatching`/`only-matching`/`-o`,只输出匹配片段而不是整行,并在 structured matches 中暴露片段 column;`-o` 同样接受 quoted boolean。
183185

184186
M5 补充:`Grep` count 输出现在支持 `count_matches`/`countMatches`/`count-matches`/`--count-matches`,需要时按匹配片段次数计数;`--count-matches` 可直接选择 count 输出,默认 count 继续保持匹配行计数,`countMatches` 同样接受 quoted boolean。

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ test/parity/ # golden tests against TS/official behavior
198198
- 本轮补充:WebFetch 现在按官方 cross-host redirect 语义处理跨 host 跳转,HEAD preflight 和 GET 都不会自动触达新 host,而是返回包含 original URL、redirect URL 和 status 的 redirect notice;同 host redirect 仍继续跟随并保留 `final_url`
199199
- 本轮补充:WebFetch input schema 现在与官方对齐,`url``prompt` 都是必填字段;既有本地扩展 `timeout``max_bytes`/`maxBytes` 仍保持可选。
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`
201+
- 本轮补充:WebFetch prompt-focused excerpt 的 term scoring 现在会匹配常见单复数变体,例如 prompt `cost` 可命中正文 `costs`,同时仍保持词边界匹配避免子串误命中。
201202
- 本轮补充:WebSearch HTML 结果解析现在会按搜索页首个有效 `<base href>` 解析相对结果 anchor,覆盖镜像/自定义搜索页中浏览器可见结果 URL 与请求路径不一致的情况。
202203
- 本轮补充:WebSearch HTML 结果解析现在会读取 `application/ld+json` JSON-LD 结果,递归抽取 `@graph``ItemList.itemListElement.item`,支持 JSON-LD `@id` URL alias,并与后续 anchor 结果按 URL 去重。
203204
- 本轮补充:WebSearch JSON parser 现在会递归解包 `web``response``search``hits``documents``records``entries` 等常见后端 wrapper,保留 URL 去重和 domain filter。

internal/tools/web/web_fetch.go

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,7 +1163,7 @@ func webFetchPromptPhrases(prompt string) []string {
11631163

11641164
func isWebFetchStopWord(word string) bool {
11651165
switch word {
1166-
case "the", "and", "for", "with", "from", "this", "that", "you", "your", "are", "was", "were", "what", "when", "where", "which", "who", "why", "how", "summarize", "summary", "about", "please":
1166+
case "the", "and", "for", "with", "from", "this", "that", "you", "your", "are", "was", "were", "what", "when", "where", "which", "who", "why", "how", "does", "did", "much", "summarize", "summary", "about", "please", "extract", "find", "show", "tell":
11671167
return true
11681168
default:
11691169
return false
@@ -1255,7 +1255,7 @@ func scoreWebFetchPassage(passage string, terms []string, phrases []string) int
12551255
}
12561256
}
12571257
for _, term := range terms {
1258-
count := termCounts[term]
1258+
count := webFetchTermMatchCount(termCounts, term)
12591259
if count > 0 {
12601260
score += 2 + count
12611261
}
@@ -1274,10 +1274,50 @@ func webFetchSearchWordCounts(normalized string) map[string]int {
12741274
counts := map[string]int{}
12751275
for _, word := range strings.Fields(normalized) {
12761276
counts[word]++
1277+
for _, variant := range webFetchWordVariants(word) {
1278+
if variant != word {
1279+
counts[variant]++
1280+
}
1281+
}
12771282
}
12781283
return counts
12791284
}
12801285

1286+
func webFetchTermMatchCount(counts map[string]int, term string) int {
1287+
best := 0
1288+
seen := map[string]bool{}
1289+
for _, variant := range webFetchWordVariants(term) {
1290+
if seen[variant] {
1291+
continue
1292+
}
1293+
seen[variant] = true
1294+
if counts[variant] > best {
1295+
best = counts[variant]
1296+
}
1297+
}
1298+
return best
1299+
}
1300+
1301+
func webFetchWordVariants(word string) []string {
1302+
word = strings.TrimSpace(strings.ToLower(word))
1303+
if word == "" {
1304+
return nil
1305+
}
1306+
variants := []string{word}
1307+
if utf8.RuneCountInString(word) <= 3 {
1308+
return variants
1309+
}
1310+
switch {
1311+
case strings.HasSuffix(word, "ies") && utf8.RuneCountInString(word) > 4:
1312+
variants = append(variants, strings.TrimSuffix(word, "ies")+"y")
1313+
case strings.HasSuffix(word, "es") && utf8.RuneCountInString(word) > 4:
1314+
variants = append(variants, strings.TrimSuffix(word, "es"))
1315+
case strings.HasSuffix(word, "s") && !strings.HasSuffix(word, "ss"):
1316+
variants = append(variants, strings.TrimSuffix(word, "s"))
1317+
}
1318+
return variants
1319+
}
1320+
12811321
func countWebFetchPhraseOccurrences(normalized string, phrase string) int {
12821322
phrase = strings.TrimSpace(strings.ToLower(phrase))
12831323
if normalized == "" || phrase == "" {

internal/tools/web/web_fetch_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,39 @@ func TestWebFetchRendersHTMLAndPromptExcerpt(t *testing.T) {
208208
}
209209
}
210210

211+
func TestWebFetchPromptExcerptMatchesPluralVariants(t *testing.T) {
212+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
213+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
214+
_, _ = w.Write([]byte(`<!doctype html>
215+
<html>
216+
<body>
217+
<main>
218+
<p>Overview page with general launch notes.</p>
219+
<p>The beta plan costs $20 and includes priority support.</p>
220+
</main>
221+
</body>
222+
</html>`))
223+
}))
224+
defer server.Close()
225+
executor := webExecutor(t)
226+
result, err := executor.Execute(tool.Context{Context: context.Background(), Metadata: map[string]any{}}, contracts.ToolUse{
227+
ID: "toolu_web_html_plural_excerpt",
228+
Name: "WebFetch",
229+
Input: json.RawMessage(`{"url":` + strconvQuote(server.URL) + `,"prompt":"cost"}`),
230+
}, nil)
231+
if err != nil {
232+
t.Fatal(err)
233+
}
234+
content := result.Content.(string)
235+
if !strings.Contains(content, "Relevant excerpt:") || !strings.Contains(content, "The beta plan costs $20") {
236+
t.Fatalf("content = %#v", content)
237+
}
238+
excerpt, ok := result.StructuredContent["prompt_excerpt"].(string)
239+
if !ok || !strings.Contains(excerpt, "The beta plan costs $20") || strings.Contains(excerpt, "Overview page") {
240+
t.Fatalf("prompt excerpt = %#v", result.StructuredContent["prompt_excerpt"])
241+
}
242+
}
243+
211244
func TestWebFetchHTMLRenderingPreservesLinksAndImageText(t *testing.T) {
212245
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
213246
w.Header().Set("Content-Type", "text/html; charset=utf-8")
@@ -548,6 +581,12 @@ func TestWebFetchPromptPhraseScoring(t *testing.T) {
548581
if score := scoreWebFetchPassage("Trust the process.", []string{"rust"}, nil); score != 0 {
549582
t.Fatalf("substring term should not match word-boundary scoring: %d", score)
550583
}
584+
if score := scoreWebFetchPassage("The beta plan costs $20.", []string{"cost"}, nil); score == 0 {
585+
t.Fatalf("singular prompt term should match plural passage word")
586+
}
587+
if score := scoreWebFetchPassage("The beta plan cost $20.", []string{"costs"}, nil); score == 0 {
588+
t.Fatalf("plural prompt term should match singular passage word")
589+
}
551590
}
552591

553592
func TestWebFetchPreflightSkipsBinaryGet(t *testing.T) {

0 commit comments

Comments
 (0)