删除冗余,修改描述
This commit is contained in:
+1
-2
@@ -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"
|
||||
|
||||
|
||||
@@ -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"] }
|
||||
@@ -1,143 +0,0 @@
|
||||
//! ias-fetch — 通过 headless Chrome 提取网页正文
|
||||
//!
|
||||
//! 启动无头 Chrome 渲染页面 JS,取渲染后的纯文本输出到 stdout。
|
||||
//! 专供 JSON 工具 `${cmd:ias-fetch {{url}}}` 调用。
|
||||
//!
|
||||
//! 用法:
|
||||
//! ias-fetch <url> # 默认:取 body.innerText
|
||||
//! ias-fetch --raw <url> # 返回完整 HTML
|
||||
//! ias-fetch --timeout 15 <url> # 自定义超时(默认 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<String> = std::env::args().collect();
|
||||
|
||||
let mut raw_mode = false;
|
||||
let mut timeout_secs = DEFAULT_TIMEOUT;
|
||||
let mut url: Option<String> = 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] <url>");
|
||||
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<String, String> {
|
||||
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::<String>(),
|
||||
content.chars().count()
|
||||
))
|
||||
} else {
|
||||
Ok(content)
|
||||
}
|
||||
}
|
||||
|
||||
/// 通过 evaluate JS 获取 body.innerText
|
||||
async fn get_inner_text(page: &Page) -> Result<String, String> {
|
||||
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()),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user