mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
feat: ship lime 1.6.1 rollout
This commit is contained in:
@@ -6739,4 +6739,34 @@ mod tests {
|
||||
.is_some());
|
||||
assert!(tools.iter().all(|tool| tool["name"] != "admin_secret"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tool_search_bridge_registration_replaces_legacy_tool_search() {
|
||||
let registry = Arc::new(tokio::sync::RwLock::new(aster::tools::ToolRegistry::new()));
|
||||
let mut guard = registry.write().await;
|
||||
|
||||
guard.register(Box::new(aster::tools::ToolSearchTool::new(
|
||||
std::sync::Weak::new(),
|
||||
)));
|
||||
assert!(guard
|
||||
.get("ToolSearch")
|
||||
.expect("legacy ToolSearch should exist")
|
||||
.description()
|
||||
.contains("Fetches full schema definitions"));
|
||||
|
||||
super::tool_runtime::search_bridge::register_tool_search_tool_to_registry(
|
||||
&mut guard,
|
||||
registry.clone(),
|
||||
None,
|
||||
);
|
||||
|
||||
let tool = guard
|
||||
.get("ToolSearch")
|
||||
.expect("bridge ToolSearch should replace legacy implementation");
|
||||
assert!(tool.description().contains("统一搜索当前会话工具面"));
|
||||
assert!(tool
|
||||
.input_schema()["properties"]
|
||||
.get("caller")
|
||||
.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,27 @@ impl ToolSearchBridgeTool {
|
||||
status.extension_name,
|
||||
)
|
||||
}
|
||||
|
||||
fn build_tool_search_notes(query: &str, hit_count: usize) -> Vec<String> {
|
||||
if hit_count > 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let trimmed = query.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
if Self::parse_select_query(trimmed).is_some() {
|
||||
return vec![
|
||||
"未命中任何工具。不要继续改写同义词反复重试;如果需要文件、命令或网页原生能力,请直接调用当前可见的 Read / Write / Edit / Glob / Grep / Bash / WebFetch / WebSearch。".to_string(),
|
||||
];
|
||||
}
|
||||
|
||||
vec![
|
||||
"未命中任何工具。优先直接调用当前可见的原生工具,或补充更明确的产品域关键词;不要继续用 ToolSearch 反复改写同义词。".to_string(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -336,10 +357,12 @@ impl Tool for ToolSearchBridgeTool {
|
||||
.take(limit)
|
||||
.map(|(_, item)| item)
|
||||
.collect::<Vec<_>>();
|
||||
let notes = Self::build_tool_search_notes(&raw_query, result.len());
|
||||
let text = serde_json::to_string_pretty(&serde_json::json!({
|
||||
"query": raw_query,
|
||||
"caller": caller,
|
||||
"count": result.len(),
|
||||
"notes": notes,
|
||||
"tools": result
|
||||
}))
|
||||
.map_err(|e| {
|
||||
@@ -355,9 +378,8 @@ pub(super) fn register_tool_search_tool_to_registry(
|
||||
registry_arc: Arc<tokio::sync::RwLock<aster::tools::ToolRegistry>>,
|
||||
extension_manager: Option<Arc<aster::agents::extension_manager::ExtensionManager>>,
|
||||
) {
|
||||
if registry.contains(TOOL_SEARCH_TOOL_NAME) {
|
||||
return;
|
||||
}
|
||||
// Lime runtime 里的 ToolSearch 事实源是 bridge 实现。
|
||||
// 这里始终重新注册,确保旧 aster ToolSearch 不会抢占当前 surface。
|
||||
registry.register(Box::new(ToolSearchBridgeTool::new(
|
||||
registry_arc,
|
||||
extension_manager,
|
||||
|
||||
@@ -328,6 +328,12 @@ enum SiteAdapterTransportRoute {
|
||||
const ADAPTER_HELPERS_SCRIPT: &str = r#"
|
||||
const helpers = {
|
||||
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
||||
resolveRoot: (value) => {
|
||||
if (value && typeof value === "object" && "nodeType" in value) {
|
||||
return value;
|
||||
}
|
||||
return document.body;
|
||||
},
|
||||
text: (value) => {
|
||||
const text = value?.textContent || "";
|
||||
return text.replace(/\s+/g, " ").trim();
|
||||
@@ -361,6 +367,205 @@ const helpers = {
|
||||
}
|
||||
return null;
|
||||
},
|
||||
waitForDomStable: async (options = {}) => {
|
||||
const root = helpers.resolveRoot(options.root);
|
||||
const stableMs = Math.max(100, Number(options.stableMs) || 500);
|
||||
const timeoutMs = Math.max(stableMs, Number(options.timeoutMs) || 2500);
|
||||
|
||||
if (!root || typeof MutationObserver === "undefined") {
|
||||
await helpers.sleep(Math.min(timeoutMs, stableMs));
|
||||
return true;
|
||||
}
|
||||
|
||||
return await new Promise((resolve) => {
|
||||
let finished = false;
|
||||
let settleTimer = null;
|
||||
let timeoutTimer = null;
|
||||
|
||||
const finish = () => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
if (settleTimer) clearTimeout(settleTimer);
|
||||
if (timeoutTimer) clearTimeout(timeoutTimer);
|
||||
observer.disconnect();
|
||||
resolve(true);
|
||||
};
|
||||
|
||||
const schedule = () => {
|
||||
if (settleTimer) clearTimeout(settleTimer);
|
||||
settleTimer = setTimeout(finish, stableMs);
|
||||
};
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
schedule();
|
||||
});
|
||||
|
||||
observer.observe(root, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
characterData: true,
|
||||
});
|
||||
|
||||
timeoutTimer = setTimeout(finish, timeoutMs);
|
||||
schedule();
|
||||
});
|
||||
},
|
||||
scrollUntilSettled: async (options = {}) => {
|
||||
const root = helpers.resolveRoot(options.root);
|
||||
const maxScrolls = Math.max(1, Number(options.maxScrolls) || 8);
|
||||
const delayMs = Math.max(80, Number(options.delayMs) || 350);
|
||||
const settleRounds = Math.max(1, Number(options.settleRounds) || 2);
|
||||
const viewportFactor = Math.max(
|
||||
0.2,
|
||||
Number(options.viewportFactor) || 0.9,
|
||||
);
|
||||
const getSnapshot =
|
||||
typeof options.getSnapshot === "function" ? options.getSnapshot : () => ({});
|
||||
const scrollingElement =
|
||||
document.scrollingElement || document.documentElement || document.body;
|
||||
let lastSignature = "";
|
||||
let lastScrollY = Math.round(window.scrollY);
|
||||
let stableCount = 0;
|
||||
let scrolls = 0;
|
||||
|
||||
while (scrolls < maxScrolls) {
|
||||
const stepPx = Math.max(
|
||||
320,
|
||||
Number(options.stepPx) || Math.round(window.innerHeight * viewportFactor),
|
||||
);
|
||||
window.scrollBy(0, stepPx);
|
||||
await helpers.sleep(delayMs);
|
||||
await helpers.waitForDomStable({
|
||||
root,
|
||||
stableMs: Math.min(600, delayMs),
|
||||
timeoutMs: Math.max(1200, delayMs * 4),
|
||||
});
|
||||
|
||||
let snapshot = {};
|
||||
try {
|
||||
snapshot = getSnapshot() || {};
|
||||
} catch {
|
||||
snapshot = {};
|
||||
}
|
||||
|
||||
const signature = JSON.stringify({
|
||||
scrollY: Math.round(window.scrollY),
|
||||
scrollHeight: Math.round(scrollingElement?.scrollHeight || 0),
|
||||
...snapshot,
|
||||
});
|
||||
|
||||
if (signature === lastSignature || Math.round(window.scrollY) === lastScrollY) {
|
||||
stableCount += 1;
|
||||
} else {
|
||||
stableCount = 0;
|
||||
}
|
||||
|
||||
lastSignature = signature;
|
||||
lastScrollY = Math.round(window.scrollY);
|
||||
scrolls += 1;
|
||||
|
||||
if (stableCount >= settleRounds) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
scrolls,
|
||||
scrollY: Math.round(window.scrollY),
|
||||
scrollHeight: Math.round(scrollingElement?.scrollHeight || 0),
|
||||
stableCount,
|
||||
};
|
||||
},
|
||||
waitForImagesReady: async (rootOrOptions, maybeOptions = {}) => {
|
||||
const root =
|
||||
rootOrOptions && typeof rootOrOptions === "object" && "nodeType" in rootOrOptions
|
||||
? rootOrOptions
|
||||
: helpers.resolveRoot(maybeOptions.root);
|
||||
const options =
|
||||
rootOrOptions && typeof rootOrOptions === "object" && "nodeType" in rootOrOptions
|
||||
? maybeOptions
|
||||
: rootOrOptions || {};
|
||||
const timeoutMs = Math.max(200, Number(options.timeoutMs) || 3000);
|
||||
const intervalMs = Math.max(80, Number(options.intervalMs) || 200);
|
||||
const stableRounds = Math.max(1, Number(options.stableRounds) || 2);
|
||||
|
||||
const collectCandidates = () => {
|
||||
const target = helpers.resolveRoot(root);
|
||||
const entries = [];
|
||||
const seen = new Set();
|
||||
const push = (element) => {
|
||||
if (!element || seen.has(element)) {
|
||||
return;
|
||||
}
|
||||
seen.add(element);
|
||||
entries.push(element);
|
||||
};
|
||||
|
||||
if (target?.matches?.("img, [role='img'], [style*='background-image']")) {
|
||||
push(target);
|
||||
}
|
||||
target
|
||||
?.querySelectorAll?.("img, [role='img'], [style*='background-image']")
|
||||
?.forEach?.((element) => push(element));
|
||||
|
||||
const details = entries.map((element) => {
|
||||
const inlineStyle = element.getAttribute?.("style") || "";
|
||||
const computedStyle =
|
||||
typeof getComputedStyle === "function"
|
||||
? getComputedStyle(element).backgroundImage || ""
|
||||
: "";
|
||||
const bgMatch = String(`${inlineStyle};${computedStyle}`).match(
|
||||
/url\((['"]?)(.*?)\1\)/,
|
||||
);
|
||||
const source =
|
||||
element.currentSrc ||
|
||||
element.getAttribute?.("src") ||
|
||||
element.getAttribute?.("data-src") ||
|
||||
element.getAttribute?.("data-image-url") ||
|
||||
element.getAttribute?.("srcset") ||
|
||||
bgMatch?.[2] ||
|
||||
"";
|
||||
return String(source || "").trim();
|
||||
});
|
||||
|
||||
return {
|
||||
total: details.length,
|
||||
ready: details.filter(Boolean).length,
|
||||
};
|
||||
};
|
||||
|
||||
const startedAt = Date.now();
|
||||
let previousSignature = "";
|
||||
let stableCount = 0;
|
||||
let lastSnapshot = collectCandidates();
|
||||
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
lastSnapshot = collectCandidates();
|
||||
const signature = JSON.stringify(lastSnapshot);
|
||||
|
||||
if (lastSnapshot.total > 0 && lastSnapshot.ready >= lastSnapshot.total) {
|
||||
return lastSnapshot;
|
||||
}
|
||||
|
||||
if (signature === previousSignature) {
|
||||
stableCount += 1;
|
||||
} else {
|
||||
stableCount = 0;
|
||||
}
|
||||
previousSignature = signature;
|
||||
|
||||
if (stableCount >= stableRounds) {
|
||||
return lastSnapshot;
|
||||
}
|
||||
|
||||
await helpers.sleep(intervalMs);
|
||||
}
|
||||
|
||||
return lastSnapshot;
|
||||
},
|
||||
take: (items, limit) => items.slice(0, Math.max(1, limit)),
|
||||
number: (value, fallbackValue) => {
|
||||
const parsed = Number(value);
|
||||
|
||||
Reference in New Issue
Block a user