删除冗余,修改描述
This commit is contained in:
+1
-2
@@ -2,7 +2,7 @@
|
|||||||
name = "iAs"
|
name = "iAs"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "微信 AI 智能助手 — 通过 DeepSeek LLM 实现 AI 自动回复"
|
description = "AI 智能助手"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# ── 异步运行时 ──
|
# ── 异步运行时 ──
|
||||||
@@ -51,4 +51,3 @@ scraper = "0.27.0" # HTML 解析(f
|
|||||||
[[bin]]
|
[[bin]]
|
||||||
name = "ias"
|
name = "ias"
|
||||||
path = "src/main.rs"
|
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