Skip to content

Commit 370167f

Browse files
committed
fix(websearch): 修复 UUID 生成逻辑,确保唯一性;更新 API Base URL 错误提示信息;新增消息引用缓存机制
1 parent 22e2c8b commit 370167f

4 files changed

Lines changed: 89 additions & 10 deletions

File tree

astrbot/builtin_stars/web_searcher/main.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ async def search_from_tavily(
396396
return "Error: Tavily web searcher does not return any results."
397397

398398
ret_ls = []
399-
ref_uuid = str(uuid.uuid4())[:4]
399+
ref_uuid = str(uuid.uuid4())
400400
for idx, result in enumerate(results, 1):
401401
index = f"{ref_uuid}.{idx}"
402402
ret_ls.append(
@@ -425,7 +425,7 @@ async def tavily_extract_web_page(
425425
"""Extract the content of a web page using Tavily.
426426
427427
Args:
428-
url(string): Required. An URl to extract content from.
428+
url(string): Required. A URL to extract content from.
429429
extract_depth(string): Optional. The depth of the extraction, must be one of 'basic', 'advanced'. Default is "basic".
430430
timeout(number): Optional. Request timeout in seconds. Minimum is 30. Default is 30.
431431
@@ -595,7 +595,7 @@ async def search_from_bocha(
595595
return "Error: BoCha web searcher does not return any results."
596596

597597
ret_ls = []
598-
ref_uuid = str(uuid.uuid4())[:4]
598+
ref_uuid = str(uuid.uuid4())
599599
for idx, result in enumerate(results, 1):
600600
index = f"{ref_uuid}.{idx}"
601601
ret_ls.append(
@@ -717,7 +717,7 @@ async def search_from_exa(
717717
return "Error: Exa web searcher does not return any results."
718718

719719
ret_ls = []
720-
ref_uuid = str(uuid.uuid4())[:4]
720+
ref_uuid = str(uuid.uuid4())
721721
for idx, result in enumerate(results, 1):
722722
index = f"{ref_uuid}.{idx}"
723723
ret_ls.append(
@@ -869,7 +869,7 @@ async def find_similar_links(
869869
return "Error: Exa find similar does not return any results."
870870

871871
ret_ls = []
872-
ref_uuid = str(uuid.uuid4())[:4]
872+
ref_uuid = str(uuid.uuid4())
873873
for idx, result in enumerate(results, 1):
874874
index = f"{ref_uuid}.{idx}"
875875
ret_ls.append(

astrbot/core/utils/web_search_utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ def normalize_web_search_base_url(
2525
parsed = urlparse(normalized)
2626
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
2727
raise ValueError(
28-
f"Error: {provider_name} API Base URL must start with http:// or https://.",
28+
f"Error: {provider_name} API Base URL must be a base host URL starting "
29+
f"with http:// or https:// (for example, {default}), not a full endpoint "
30+
f"path. Received: {normalized!r}.",
2931
)
3032
return normalized
3133

dashboard/src/components/chat/MessageList.vue

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,8 @@ export default {
274274
url: ''
275275
},
276276
// Web search results mapping: { 'uuid.idx': { url, title, snippet } }
277-
webSearchResults: {}
277+
webSearchResults: {},
278+
messageRefsCache: new WeakMap()
278279
};
279280
},
280281
async mounted() {
@@ -359,13 +360,66 @@ export default {
359360
return refs;
360361
},
361362
363+
buildMessageRefsCacheKey(messageParts) {
364+
if (!Array.isArray(messageParts)) {
365+
return '';
366+
}
367+
368+
const cacheParts = [];
369+
370+
messageParts.forEach(part => {
371+
if (part.type !== 'tool_call' || !Array.isArray(part.tool_calls)) {
372+
return;
373+
}
374+
375+
part.tool_calls.forEach(toolCall => {
376+
if (!WEB_SEARCH_REFERENCE_TOOLS.includes(toolCall?.name) || !toolCall.result) {
377+
return;
378+
}
379+
380+
const rawResult = typeof toolCall.result === 'string'
381+
? toolCall.result
382+
: JSON.stringify(toolCall.result);
383+
384+
cacheParts.push(`${toolCall.id || toolCall.name}:${rawResult}`);
385+
});
386+
});
387+
388+
return cacheParts.join('||');
389+
},
390+
391+
getCachedMessageRefs(content) {
392+
if (!content || typeof content !== 'object') {
393+
return null;
394+
}
395+
396+
const cacheKey = this.buildMessageRefsCacheKey(content.message);
397+
if (!cacheKey) {
398+
return null;
399+
}
400+
401+
const cachedEntry = this.messageRefsCache.get(content);
402+
if (cachedEntry?.key === cacheKey) {
403+
return cachedEntry.refs;
404+
}
405+
406+
const refs = this.collectMessageWebSearchRefs(content.message);
407+
const normalizedRefs = refs.length ? { used: refs } : null;
408+
409+
this.messageRefsCache.set(content, {
410+
key: cacheKey,
411+
refs: normalizedRefs
412+
});
413+
414+
return normalizedRefs;
415+
},
416+
362417
getMessageRefs(content) {
363418
if (content?.refs?.used?.length) {
364419
return content.refs;
365420
}
366421
367-
const fallbackRefs = this.collectMessageWebSearchRefs(content?.message);
368-
return fallbackRefs.length ? { used: fallbackRefs } : null;
422+
return this.getCachedMessageRefs(content);
369423
},
370424
371425
// 从消息中提取网页搜索结果映射
@@ -377,7 +431,12 @@ export default {
377431
return;
378432
}
379433
380-
this.collectMessageWebSearchRefs(msg.content.message).forEach(ref => {
434+
const refs = this.getMessageRefs(msg.content);
435+
if (!refs?.used?.length) {
436+
return;
437+
}
438+
439+
refs.used.forEach(ref => {
381440
results[ref.index] = {
382441
url: ref.url,
383442
title: ref.title,

tests/unit/test_web_search_utils.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import json
22

3+
import pytest
4+
35
from astrbot.core.utils.web_search_utils import (
46
build_web_search_refs,
57
collect_web_search_ref_items,
68
collect_web_search_results,
9+
normalize_web_search_base_url,
710
)
811

912

@@ -88,3 +91,18 @@ def test_build_web_search_refs_ignores_tool_call_id_and_falls_back():
8891
)
8992

9093
assert [ref["index"] for ref in refs["used"]] == ["a152.1", "a152.2"]
94+
95+
96+
def test_normalize_web_search_base_url_reports_invalid_value():
97+
with pytest.raises(ValueError) as exc_info:
98+
normalize_web_search_base_url(
99+
"exa.ai/search",
100+
default="https://api.exa.ai",
101+
provider_name="Exa",
102+
)
103+
104+
assert str(exc_info.value) == (
105+
"Error: Exa API Base URL must be a base host URL starting with "
106+
"http:// or https:// (for example, https://api.exa.ai), not a full "
107+
"endpoint path. Received: 'exa.ai/search'."
108+
)

0 commit comments

Comments
 (0)