Compare commits
10 Commits
99d212d26a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d25aeb272 | |||
|
|
bfbf9adf53 | ||
|
|
60cd6097e4 | ||
|
|
1ebccef389 | ||
|
|
0b036a6a63 | ||
|
|
e45a391458 | ||
|
|
d61262215e | ||
| ed6f65c43f | |||
|
|
916209c567 | ||
|
|
9dcf84dcb3 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -16,3 +16,6 @@ target/
|
|||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
|
# Do not include environment files
|
||||||
|
.env
|
||||||
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Ignored default folder with query files
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal 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
7
.idea/dictionaries/project.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<component name="ProjectDictionaryState">
|
||||||
|
<dictionary name="project">
|
||||||
|
<words>
|
||||||
|
<w>feishu</w>
|
||||||
|
</words>
|
||||||
|
</dictionary>
|
||||||
|
</component>
|
||||||
14
.idea/discord.xml
generated
Normal file
14
.idea/discord.xml
generated
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DiscordProjectSettings">
|
||||||
|
<option name="show" value="PROJECT_FILES" />
|
||||||
|
<option name="description" value="" />
|
||||||
|
<option name="applicationTheme" value="default" />
|
||||||
|
<option name="iconsTheme" value="default" />
|
||||||
|
<option name="button1Title" value="" />
|
||||||
|
<option name="button1Url" value="" />
|
||||||
|
<option name="button2Title" value="" />
|
||||||
|
<option name="button2Url" value="" />
|
||||||
|
<option name="customApplicationId" value="" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
21
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
21
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="myValues">
|
||||||
|
<value>
|
||||||
|
<list size="7">
|
||||||
|
<item index="0" class="java.lang.String" itemvalue="nobr" />
|
||||||
|
<item index="1" class="java.lang.String" itemvalue="noembed" />
|
||||||
|
<item index="2" class="java.lang.String" itemvalue="comment" />
|
||||||
|
<item index="3" class="java.lang.String" itemvalue="noscript" />
|
||||||
|
<item index="4" class="java.lang.String" itemvalue="embed" />
|
||||||
|
<item index="5" class="java.lang.String" itemvalue="script" />
|
||||||
|
<item index="6" class="java.lang.String" itemvalue="submit" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
<option name="myCustomValuesEnabled" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/rss-to-feishu.iml" filepath="$PROJECT_DIR$/.idea/rss-to-feishu.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
11
.idea/rss-to-feishu.iml
generated
Normal file
11
.idea/rss-to-feishu.iml
generated
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="EMPTY_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
2432
Cargo.lock
generated
Normal file
2432
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
Normal file
12
Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "rss-to-feishu"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[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
28
Dockerfile
Normal 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
11
assets/config.yaml
Normal 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
36
src/api/client.rs
Normal 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
1
src/api/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub(crate) mod client;
|
||||||
38
src/config.rs
Normal file
38
src/config.rs
Normal 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
40
src/controls/feishu.rs
Normal 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
2
src/controls/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod feishu;
|
||||||
|
pub mod summary;
|
||||||
28
src/controls/summary.rs
Normal file
28
src/controls/summary.rs
Normal 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)
|
||||||
|
}
|
||||||
17
src/main.rs
Normal file
17
src/main.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
mod api;
|
||||||
|
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
8
src/models/feishu.rs
Normal 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
3
src/models/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod feishu;
|
||||||
|
pub mod silicon_cloud;
|
||||||
|
pub mod summary;
|
||||||
18
src/models/silicon_cloud.rs
Normal file
18
src/models/silicon_cloud.rs
Normal 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
10
src/models/summary.rs
Normal 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user