feat: ship lime 1.6.1 rollout

This commit is contained in:
coso
2026-04-09 18:47:33 +08:00
parent db3bfae4ff
commit 182c0d464a
152 changed files with 19877 additions and 2413 deletions
@@ -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);