Compare commits

...

7 Commits

Author SHA1 Message Date
1d25aeb272 Merge pull request 'feat: summarize rss messages to feishu group' (#1) from develop into main
Reviewed-on: #1
2026-01-13 15:04:21 +08:00
ECSS 11
bfbf9adf53 chore: docker build config 2026-01-13 00:56:25 -06:00
ECSS 11
60cd6097e4 refactor: crate use and mcv 2026-01-13 00:55:51 -06:00
ECSS 11
1ebccef389 refactor: style overhauls 2026-01-13 00:46:00 -06:00
ECSS 11
0b036a6a63 refactor: optimize use 2026-01-13 00:45:23 -06:00
ECSS 11
e45a391458 chore: idea struct 2026-01-13 00:45:03 -06:00
ECSS 11
d61262215e feat: summarize rss and send to feishu 2026-01-13 00:44:52 -06:00
19 changed files with 2683 additions and 7 deletions

2
.idea/.gitignore generated vendored
View File

@@ -1,4 +1,4 @@
# Default ignored files # Default ignored files
/shelf/ /shelf/
/workspace.xml /workspace.xml
# Ignored default folder with query files # Ignored default folder with query files

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

7
.idea/dictionaries/project.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
<w>feishu</w>
</words>
</dictionary>
</component>

4
.idea/encodings.xml generated
View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>

2425
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,3 +4,9 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
tokio = { version = "1.49.0", features = ["rt", "rt-multi-thread", "macros"] }
reqwest = { version = "0.13.1", features = ["default", "json"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = {version = "1.0.149", features = ["default"]}
serde_yaml = "0.9.33"
open-lark = "0.14.0"

28
Dockerfile Normal file
View File

@@ -0,0 +1,28 @@
FROM rust:1.80-slim AS builder
# Dummy app
RUN USER=root cargo new --bin app
WORKDIR /app
# Cargo build and src cleaup
COPY ./Cargo.toml ./Cargo.toml
COPY ./Cargo.lock ./Cargo.lock
RUN cargo build --release
RUN rm src/*.rs
# Copy src files
COPY ./src ./src
RUN rm ./target/release/deps/app*
RUN cargo build --release
FROM debian:bookworm-slim
# Install necessary runtime libraries
RUN apt-get update && apt-get install -y libssl-dev ca-certificates && rm -rf /var/lib/apt/lists/*
# Copy the binary from the builder stage
COPY --from=builder /app/target/release/app /usr/local/bin/app
# Set execution permissions and run
CMD ["app"]

11
assets/config.yaml Normal file
View File

@@ -0,0 +1,11 @@
subscribes:
- name: "cvs-feed"
url: "https://cvefeed.io/rssfeed/latest.xml"
prompt: "Summarize the security list in Chinese."
feishu:
app-id: ""
app-secret: ""
chat-id: ""
token: ""

36
src/api/client.rs Normal file
View File

@@ -0,0 +1,36 @@
pub async fn get(url: &String) -> Result<String, Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let response = client.get(url).send().await?;
let body = response.text().await?;
Ok(body)
}
pub async fn request_model(
prompt: &String,
token: &String,
) -> Result<String, Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let body = serde_json::json!({
"model": "Qwen/Qwen3-8B",
"messages": [{
"role": "user",
"content": prompt
}],
"max_tokens": 8192
});
let response = client
.post("https://api.siliconflow.cn/v1/messages")
.header("Authorization", format!("Bearer {}", token))
.header("Content-Type", "application/json")
.json(&body)
.send()
.await?;
let body = response.text().await?;
Ok(body)
}

1
src/api/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub(crate) mod client;

38
src/config.rs Normal file
View File

@@ -0,0 +1,38 @@
use std::fs;
#[derive(Debug, serde::Deserialize)]
pub struct Config {
pub subscribes: Vec<SubscribeConfig>,
pub feishu: FeishuConfig,
pub token: String,
}
#[derive(Debug, serde::Deserialize, Clone)]
pub struct SubscribeConfig {
pub name: String,
pub url: String,
pub prompt: String,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct FeishuConfig {
pub app_id: String,
pub app_secret: String,
pub chat_id: String,
}
impl Config {
pub fn build() -> Result<Self, Box<dyn std::error::Error>> {
let yaml_config = fs::read_to_string("./assets/config.yaml")?;
let mut config: Config = serde_yaml::from_str(&yaml_config)?;
// Load env params to yaml.
config.token = std::env::var("TOKEN")?;
config.feishu.app_id = std::env::var("APP_ID")?;
config.feishu.app_secret = std::env::var("APP_SECRET")?;
config.feishu.chat_id = std::env::var("CHAT_ID")?;
Ok(config)
}
}

40
src/controls/feishu.rs Normal file
View File

@@ -0,0 +1,40 @@
use crate::{config, models};
use crate::models::feishu::FeishuClient;
impl FeishuClient {
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
let config = config::Config::build()?;
let client = open_lark::prelude::LarkClient::builder(
config.feishu.app_id.as_str(),
config.feishu.app_secret.as_str(),
)
.build();
Ok(Self { client })
}
pub async fn send_message(&self, message: &String) -> Result<(), Box<dyn std::error::Error>> {
let config = config::Config::build()?;
let content = models::feishu::MessageContent {
text: message.to_string(),
};
let content = serde_json::to_string(&content)?;
let message = open_lark::prelude::CreateMessageRequestBody::builder()
.receive_id(config.feishu.chat_id)
.msg_type("text")
.content(content)
.build();
let request = open_lark::prelude::CreateMessageRequest::builder()
.receive_id_type("chat_id")
.request_body(message)
.build();
self.client.im.v1.message.create(request, None).await?;
Ok(())
}
}

2
src/controls/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod feishu;
pub mod summary;

28
src/controls/summary.rs Normal file
View File

@@ -0,0 +1,28 @@
use crate::api;
use crate::models;
use std::error::Error;
use crate::config::Config;
pub async fn summarize_rss() -> Result<Vec<models::summary::Summary>, Box<dyn Error>> {
let config = Config::build()?;
let mut summary_list: Vec<models::summary::Summary> = Vec::new();
let token = &config.token;
let subscribes = &config.subscribes;
for subscribe in subscribes {
// Get rss message and prompt.
let rss_message = api::client::get(&subscribe.url).await?;
let prompt = format!("{} \n {}", &subscribe.prompt, rss_message);
// Get response from remote AI model.
let model_response = api::client::request_model(&prompt, token).await?;
let parse_model_response = models::silicon_cloud::ModelResponse::build(model_response)?;
let message = &parse_model_response.content.first().unwrap().text;
// Save summary result in list.
let summary = models::summary::Summary::new(subscribe.name.clone(), message.to_string());
summary_list.push(summary);
}
Ok(summary_list)
}

View File

@@ -1,3 +1,17 @@
fn main() { mod api;
println!("Hello, world!"); mod config;
mod controls;
mod models;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let summary_list = controls::summary::summarize_rss().await?;
let app = models::feishu::FeishuClient::new()?;
for summary in summary_list {
app.send_message(&summary.name).await?;
app.send_message(&summary.description).await?;
}
Ok(())
} }

8
src/models/feishu.rs Normal file
View File

@@ -0,0 +1,8 @@
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct MessageContent {
pub text: String,
}
pub struct FeishuClient {
pub client: open_lark::prelude::LarkClient,
}

3
src/models/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod feishu;
pub mod silicon_cloud;
pub mod summary;

View File

@@ -0,0 +1,18 @@
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Content {
pub text: String,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub struct ModelResponse {
pub id: String,
pub content: Vec<Content>,
pub model: String,
}
impl ModelResponse {
pub fn build(response: String) -> Result<Self, Box<dyn std::error::Error>> {
let prase_response: ModelResponse = serde_json::from_str(&response)?;
Ok(prase_response)
}
}

10
src/models/summary.rs Normal file
View File

@@ -0,0 +1,10 @@
pub struct Summary {
pub name: String,
pub description: String,
}
impl Summary {
pub fn new(name: String, description: String) -> Self {
Self { name, description }
}
}