first commit
This commit is contained in:
commit
0ee735299e
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
target
|
||||
node_modules
|
||||
.wrangler
|
1397
Cargo.lock
generated
Normal file
1397
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
Cargo.toml
Normal file
30
Cargo.toml
Normal file
@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "miniflux-ai"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = [ "zhu327" ]
|
||||
|
||||
[package.metadata.release]
|
||||
release = false
|
||||
|
||||
# https://github.com/rustwasm/wasm-pack/issues/1247
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt = false
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
strip = true
|
||||
codegen-units = 1
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.21"
|
||||
worker = { version="0.3.4" }
|
||||
worker-macros = { version="0.3.4" }
|
||||
console_error_panic_hook = { version = "0.1.7" }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
futures = "0.3"
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Timmy
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
57
README.md
Normal file
57
README.md
Normal file
@ -0,0 +1,57 @@
|
||||
# Miniflux AI Summarizer
|
||||
|
||||
This Cloudflare Workers tool automatically adds AI-generated summaries to articles in your Miniflux RSS reader. The summaries are generated using the OpenAI API and appended to articles in a user-friendly format.
|
||||
|
||||
## Features
|
||||
|
||||
- **Automated Summarization**: Fetches unread articles from Miniflux, generates concise summaries using AI, and updates the articles with the summaries.
|
||||
- **Customizable**: Configure the list of whitelisted websites, API endpoints, and AI model parameters through environment variables.
|
||||
- **Concurrency**: Uses asynchronous Rust features to handle multiple articles concurrently, ensuring quick processing.
|
||||
- **Cloudflare Integration**: Deployed as a serverless function on Cloudflare Workers, leveraging the scalability and performance of Cloudflare's global network.
|
||||
- **Recommended Model**: Uses the Cloudflare Workers AI model `@cf/qwen/qwen1.5-14b-chat-awq` for generating high-quality, concise summaries.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Rust](https://www.rust-lang.org/tools/install) installed
|
||||
- A Miniflux instance with API access
|
||||
- An OpenAI account with access to the model endpoint
|
||||
- A Cloudflare account
|
||||
|
||||
### Installation
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/yourusername/miniflux-ai.git
|
||||
cd miniflux-ai
|
||||
```
|
||||
|
||||
2. Deploy to Cloudflare Workers:
|
||||
```bash
|
||||
npx wrangler deploy
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
The tool is configured using environment variables, which are set in the `wrangler.toml` file:
|
||||
|
||||
- `MINIFLUX_URL`: Your Miniflux instance URL.
|
||||
- `MINIFLUX_USERNAME`: Your Miniflux username.
|
||||
- `MINIFLUX_PASSWORD`: Your Miniflux password.
|
||||
- `OPENAI_URL`: The endpoint for the OpenAI API.
|
||||
- `OPENAI_TOKEN`: Your OpenAI API token.
|
||||
- `OPENAI_MODEL`: The model ID to use for generating summaries. We recommend using the `@cf/qwen/qwen1.5-14b-chat-awq` model for best results.
|
||||
- `WHITELIST_URL`: A comma-separated list of website URLs that should be summarized.
|
||||
|
||||
### Usage
|
||||
|
||||
The tool runs as a scheduled Cloudflare Worker, querying for unread articles every 5 minutes. If an article is from a whitelisted site and does not contain code blocks, it generates a summary and updates the article.
|
||||
|
||||
### Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit issues, feature requests, or pull requests.
|
||||
|
||||
### License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
248
src/lib.rs
Normal file
248
src/lib.rs
Normal file
@ -0,0 +1,248 @@
|
||||
use base64;
|
||||
use futures::future::join_all;
|
||||
use reqwest::header::{AUTHORIZATION, CONTENT_TYPE};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use worker::{event, Env, ScheduleContext, ScheduledEvent};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Feed {
|
||||
site_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Entry {
|
||||
id: u64,
|
||||
content: String,
|
||||
feed: Feed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ApiResponse {
|
||||
entries: Vec<Entry>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct UpdateRequest {
|
||||
content: String,
|
||||
}
|
||||
|
||||
async fn get_entries(
|
||||
base_url: &str,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<ApiResponse, Box<dyn std::error::Error>> {
|
||||
// 创建 HTTP 客户端
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// 使用 Basic Auth 进行身份验证
|
||||
let auth = format!(
|
||||
"Basic {}",
|
||||
base64::encode(format!("{}:{}", username, password))
|
||||
);
|
||||
|
||||
// 发送 GET 请求
|
||||
let response = client
|
||||
.get(&format!("{}/v1/entries?status=unread&limit=100", base_url))
|
||||
.header(AUTHORIZATION, auth)
|
||||
.header(CONTENT_TYPE, "application/json")
|
||||
.send()
|
||||
.await?
|
||||
.json::<ApiResponse>()
|
||||
.await?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn update_entry(
|
||||
base_url: &str,
|
||||
username: &str,
|
||||
password: &str,
|
||||
id: u64,
|
||||
content: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = reqwest::Client::new();
|
||||
let auth = format!(
|
||||
"Basic {}",
|
||||
base64::encode(format!("{}:{}", username, password))
|
||||
);
|
||||
|
||||
let url = format!("{}/v1/entries/{}", base_url, id);
|
||||
let update_request = UpdateRequest {
|
||||
content: content.to_string(),
|
||||
};
|
||||
|
||||
client
|
||||
.put(&url)
|
||||
.header(AUTHORIZATION, auth)
|
||||
.header(CONTENT_TYPE, "application/json")
|
||||
.json(&update_request) // 将请求体序列化为 JSON
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ChatCompletionRequest {
|
||||
model: String,
|
||||
messages: Vec<Message>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Message {
|
||||
role: String,
|
||||
content: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ChatCompletionChoice {
|
||||
message: Message,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ChatCompletionResponse {
|
||||
choices: Vec<ChatCompletionChoice>,
|
||||
}
|
||||
|
||||
async fn request_openai_chat_completion(
|
||||
base_url: &str,
|
||||
api_key: &str,
|
||||
model: &str,
|
||||
messages: Vec<Message>,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let client = reqwest::Client::new();
|
||||
let request_body = ChatCompletionRequest {
|
||||
model: model.to_string(),
|
||||
messages,
|
||||
};
|
||||
|
||||
let response = client
|
||||
.post(&format!("{}/v1/chat/completions", base_url))
|
||||
.header(AUTHORIZATION, format!("Bearer {}", api_key))
|
||||
.header(CONTENT_TYPE, "application/json")
|
||||
.json(&request_body)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
let completion_response: ChatCompletionResponse = response.json().await?;
|
||||
Ok(completion_response.choices[0].message.content.clone())
|
||||
} else {
|
||||
let error_message = response.text().await?;
|
||||
Err(format!("Error: {:?}", error_message).into())
|
||||
}
|
||||
}
|
||||
|
||||
struct Miniflux {
|
||||
url: String,
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
struct OpenAi {
|
||||
url: String,
|
||||
token: String,
|
||||
model: String,
|
||||
}
|
||||
|
||||
struct Config {
|
||||
miniflux: Miniflux,
|
||||
openai: OpenAi,
|
||||
whitelist: HashSet<String>,
|
||||
}
|
||||
|
||||
async fn generate_and_update_entry(
|
||||
config: &Config,
|
||||
entry: Entry,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let content: &str = &entry.content;
|
||||
// Check if the content should be summarized and if the site is whitelisted
|
||||
if content.starts_with("<pre") || !config.whitelist.contains(&entry.feed.site_url) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let messages = vec![
|
||||
Message {
|
||||
role: "system".to_string(),
|
||||
content: "Please summarize the content of the article under 50 words in Chinese. Do not add any additional Character、markdown language to the result text. 请用不超过50个汉字概括文章内容。结果文本中不要添加任何额外的字符、Markdown语言。".to_string(),
|
||||
},
|
||||
Message {
|
||||
role: "user".to_string(),
|
||||
content: format!(
|
||||
"The following is the input content:\n---\n {}",
|
||||
content,
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// Generate summary
|
||||
if let Ok(summary) = request_openai_chat_completion(
|
||||
&config.openai.url,
|
||||
&config.openai.token,
|
||||
&config.openai.model,
|
||||
messages,
|
||||
)
|
||||
.await
|
||||
{
|
||||
let updated_content = format!(
|
||||
"<pre style=\"white-space: pre-wrap;\"><code>\n💡AI 摘要:\n{}</code></pre><hr><br />{}",
|
||||
summary, content
|
||||
);
|
||||
|
||||
// Update the entry
|
||||
update_entry(
|
||||
&config.miniflux.url,
|
||||
&config.miniflux.username,
|
||||
&config.miniflux.password,
|
||||
entry.id,
|
||||
&updated_content,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[event(scheduled)]
|
||||
async fn scheduled(_event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
|
||||
let config = &Config {
|
||||
whitelist: env
|
||||
.var("WHITELIST_URL")
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.split(",")
|
||||
.map(|s| s.to_string())
|
||||
.collect(),
|
||||
openai: OpenAi {
|
||||
url: env.var("OPENAI_URL").unwrap().to_string(),
|
||||
token: env.var("OPENAI_TOKEN").unwrap().to_string(),
|
||||
model: env.var("OPENAI_MODEL").unwrap().to_string(),
|
||||
},
|
||||
miniflux: Miniflux {
|
||||
url: env.var("MINIFLUX_URL").unwrap().to_string(),
|
||||
username: env.var("MINIFLUX_USERNAME").unwrap().to_string(),
|
||||
password: env.var("MINIFLUX_PASSWORD").unwrap().to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
// 查询未读文章
|
||||
let entries = get_entries(
|
||||
&config.miniflux.url,
|
||||
&config.miniflux.username,
|
||||
&config.miniflux.password,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 生成摘要并更新的并发任务
|
||||
let tasks: Vec<_> = entries
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(|entry| generate_and_update_entry(&config, entry))
|
||||
.collect();
|
||||
|
||||
// 执行所有任务并等待结果
|
||||
join_all(tasks).await;
|
||||
}
|
18
wrangler.toml
Normal file
18
wrangler.toml
Normal file
@ -0,0 +1,18 @@
|
||||
name = "miniflux-ai"
|
||||
main = "build/worker/shim.mjs"
|
||||
compatibility_date = "2024-08-06"
|
||||
|
||||
[build]
|
||||
command = "cargo install -q worker-build && worker-build --release"
|
||||
|
||||
[triggers]
|
||||
crons = ["*/5 * * * *"]
|
||||
|
||||
[vars]
|
||||
MINIFLUX_URL = "your miniflux url"
|
||||
MINIFLUX_USERNAME = "your miniflux username"
|
||||
MINIFLUX_PASSWORD = "your miniflux password"
|
||||
OPENAI_URL = "https://api.cloudflare.com/client/v4/accounts/{your cloudflare account}/ai"
|
||||
OPENAI_TOKEN = "your cloudflare workers AI token"
|
||||
OPENAI_MODEL = "@cf/qwen/qwen1.5-14b-chat-awq"
|
||||
WHITELIST_URL = "https://www.zaobao.com/news/china,https://t.me/s/theinitium_rss,https://cn.nytimes.com,https://www.latepost.com/news/get-news-data,https://t.me/s/wsj_rss,http://www.zhihu.com,https://new.qq.com/omn/author/5157372,https://www.huxiu.com"
|
Loading…
Reference in New Issue
Block a user