diff --git a/Cargo.toml b/Cargo.toml index c45d272..80fb80d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "iAs" version = "0.1.0" edition = "2024" -description = "微信 AI 智能助手 — 通过 DeepSeek LLM 实现 AI 自动回复" +description = "AI 智能助手" [dependencies] # ── 异步运行时 ── @@ -51,4 +51,3 @@ scraper = "0.27.0" # HTML 解析(f [[bin]] name = "ias" path = "src/main.rs" - diff --git a/crates/ias-fetch/Cargo.toml b/crates/ias-fetch/Cargo.toml deleted file mode 100644 index f2e3df8..0000000 --- a/crates/ias-fetch/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "ias-fetch" -version = "0.1.0" -edition = "2024" - -[dependencies] -chromiumoxide = { version = "0.7", default-features = false, features = ["tokio-runtime", "bytes"] } -futures = "0.3" -serde_json = "1.0" -tokio = { version = "1.0", features = ["full"] } diff --git a/crates/ias-fetch/src/main.rs b/crates/ias-fetch/src/main.rs deleted file mode 100644 index f500959..0000000 --- a/crates/ias-fetch/src/main.rs +++ /dev/null @@ -1,143 +0,0 @@ -//! ias-fetch — 通过 headless Chrome 提取网页正文 -//! -//! 启动无头 Chrome 渲染页面 JS,取渲染后的纯文本输出到 stdout。 -//! 专供 JSON 工具 `${cmd:ias-fetch {{url}}}` 调用。 -//! -//! 用法: -//! ias-fetch # 默认:取 body.innerText -//! ias-fetch --raw # 返回完整 HTML -//! ias-fetch --timeout 15 # 自定义超时(默认 15s) - -use futures::StreamExt; -use std::time::Duration; - -use chromiumoxide::{Browser, BrowserConfig, Page}; - -const DEFAULT_TIMEOUT: u64 = 15; -const MAX_OUTPUT_CHARS: usize = 10_000; - -#[tokio::main] -async fn main() { - let args: Vec = std::env::args().collect(); - - let mut raw_mode = false; - let mut timeout_secs = DEFAULT_TIMEOUT; - let mut url: Option = None; - - let mut i = 1; - while i < args.len() { - match args[i].as_str() { - "--raw" => raw_mode = true, - "--timeout" => { - i += 1; - timeout_secs = args.get(i).and_then(|s| s.parse().ok()).unwrap_or(DEFAULT_TIMEOUT); - } - s if s.starts_with("http://") || s.starts_with("https://") => { - url = Some(s.to_string()); - } - s if !s.starts_with('-') => url = Some(s.to_string()), - _ => {} - } - i += 1; - } - - let url = match url { - Some(u) => { - if u.starts_with("http://") || u.starts_with("https://") { - u - } else { - format!("https://{}", u) - } - } - None => { - eprintln!("用法: ias-fetch [--raw] [--timeout N] "); - std::process::exit(1); - } - }; - - let timeout = Duration::from_secs(timeout_secs); - - match tokio::time::timeout(timeout, fetch_page(&url, raw_mode)).await { - Ok(Ok(output)) => { - if output.is_empty() { - eprintln!("页面无内容"); - std::process::exit(1); - } - println!("{}", output); - } - Ok(Err(e)) => { - eprintln!("{}", e); - std::process::exit(1); - } - Err(_) => { - eprintln!("超时 ({}s): {}", timeout_secs, url); - std::process::exit(1); - } - } -} - -async fn fetch_page(url: &str, raw_mode: bool) -> Result { - let (mut browser, mut handler) = Browser::launch( - BrowserConfig::builder() - .build() - .map_err(|e| format!("Chrome 配置失败: {}", e))?, - ) - .await - .map_err(|e| format!("启动 Chrome 失败: {}\n请确保已安装 chromium 或 google-chrome", e))?; - - let handle = tokio::spawn(async move { - while let Some(_event) = handler.next().await { - // 持续消费 handler 事件 - } - }); - - // new_page 接受 URL,自动导航并等待加载 - let page = browser - .new_page(url) - .await - .map_err(|e| format!("导航到 {} 失败: {}", url, e))?; - - // 额外等待 JS 渲染 - tokio::time::sleep(Duration::from_secs(2)).await; - - // 提取内容 - let content = if raw_mode { - page.content().await.map_err(|e| format!("获取 HTML 失败: {}", e))? - } else { - get_inner_text(&page).await? - }; - - // 清理 - browser.close().await.ok(); - handle.await.ok(); - - // 截断 - if content.chars().count() > MAX_OUTPUT_CHARS { - Ok(format!( - "{}…(已截断,完整内容 {} 字符)", - content.chars().take(MAX_OUTPUT_CHARS).collect::(), - content.chars().count() - )) - } else { - Ok(content) - } -} - -/// 通过 evaluate JS 获取 body.innerText -async fn get_inner_text(page: &Page) -> Result { - let result = page - .evaluate("document.body?.innerText || document.documentElement?.innerText || ''") - .await - .map_err(|e| format!("提取正文失败: {}", e))?; - - let value: serde_json::Value = result - .into_value() - .map_err(|e| format!("解析 evaluate 结果失败: {}", e))?; - match value { - serde_json::Value::String(s) => { - let cleaned: Vec<&str> = s.lines().map(|l| l.trim()).filter(|l| !l.is_empty()).collect(); - Ok(cleaned.join("\n")) - } - _ => Ok(String::new()), - } -}