feat: 新增Rust项目基础框架和核心功能模块
- 添加Cargo.toml配置文件,定义项目元信息和依赖项 - 实现配置管理模块(ConfigManager),支持JSON配置读写 - 添加爬虫模块(SpiderManager),支持网页内容抓取和解析 - 实现数据库模块(DatabaseManager),使用SQLite存储评论数据 - 添加LLM分析模块(LLMAnalyzer),支持调用AI接口进行情绪分析 - 实现UI界面模块,包含指标显示和波形图绘制功能 - 添加项目文档和截图资源
This commit is contained in:
8
rust/.cargo/config.toml
Normal file
8
rust/.cargo/config.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[source.crates-io]
|
||||
replace-with = "ustc"
|
||||
|
||||
[source.ustc]
|
||||
registry = "sparse+https://mirrors.ustc.edu.cn/crates.io-index/"
|
||||
|
||||
[http]
|
||||
check-revoke = false
|
||||
5346
rust/Cargo.lock
generated
Normal file
5346
rust/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
42
rust/Cargo.toml
Normal file
42
rust/Cargo.toml
Normal file
@@ -0,0 +1,42 @@
|
||||
[package]
|
||||
name = "guba"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Guba Developer"]
|
||||
description = "股吧人气指示器 - 基于Rust的情感分析工具"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
sha2 = "0.10"
|
||||
hex = "0.4"
|
||||
regex = "1.10"
|
||||
thiserror = "2.0"
|
||||
once_cell = "1.19"
|
||||
parking_lot = "0.12"
|
||||
dirs = "6.0"
|
||||
|
||||
[dependencies.egui]
|
||||
version = "0.29"
|
||||
features = ["default", "persistence"]
|
||||
|
||||
[dependencies.eframe]
|
||||
version = "0.29"
|
||||
default-features = false
|
||||
features = ["default", "glow"]
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
opt-level = "z"
|
||||
strip = true
|
||||
182
rust/src/analyzer.rs
Normal file
182
rust/src/analyzer.rs
Normal file
@@ -0,0 +1,182 @@
|
||||
use crate::config::LlmApiConfig;
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AnalyzerError {
|
||||
#[error("Request failed: {0}")]
|
||||
RequestError(#[from] reqwest::Error),
|
||||
#[error("API error: {0}")]
|
||||
ApiError(String),
|
||||
#[error("Parse error: {0}")]
|
||||
ParseError(String),
|
||||
#[error("No API key configured")]
|
||||
NoApiKey,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AnalysisResponse {
|
||||
pub score: Option<i32>,
|
||||
pub label: Option<String>,
|
||||
pub reasoning: Option<String>,
|
||||
}
|
||||
|
||||
pub struct LLMAnalyzer {
|
||||
config: LlmApiConfig,
|
||||
client: Client,
|
||||
last_result: Option<AnalysisResponse>,
|
||||
}
|
||||
|
||||
impl LLMAnalyzer {
|
||||
pub fn new(config: LlmApiConfig) -> Self {
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(config.timeout))
|
||||
.build()
|
||||
.expect("Failed to create HTTP client");
|
||||
|
||||
Self {
|
||||
config,
|
||||
client,
|
||||
last_result: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn analyze(&mut self, content: &str) -> Result<(i32, String), AnalyzerError> {
|
||||
if self.config.api_key.is_empty() {
|
||||
return Ok((50, "未配置API".to_string()));
|
||||
}
|
||||
|
||||
let prompt = self.build_prompt(content);
|
||||
let response = self.send_request(&prompt)?;
|
||||
|
||||
if let Some(score) = response.score {
|
||||
let label = response.label.unwrap_or_else(|| self.get_label(score));
|
||||
self.last_result = Some(response);
|
||||
Ok((score, label))
|
||||
} else {
|
||||
Ok((50, "无法判断".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn build_prompt(&self, content: &str) -> String {
|
||||
format!(
|
||||
r#"你是一个专业的股市情绪分析师。请分析以下股评内容的情绪倾向,并给出0-100分的情绪评分。
|
||||
|
||||
评分标准:
|
||||
- 0-30分:极度悲观(股灾、暴跌、绝望等)
|
||||
- 30-39分:悲观(下跌、风险、不看好等)
|
||||
- 40-49分:偏悲观(谨慎、担忧等)
|
||||
- 50分:中性(观望、震荡等)
|
||||
- 51-60分:偏乐观(关注、期待等)
|
||||
- 60-69分:乐观(看好、上涨等)
|
||||
- 70-100分:极度乐观(暴涨、牛市、必涨等)
|
||||
|
||||
请直接返回JSON格式的分析结果,不要其他内容:
|
||||
{{"score": 评分数字, "label": "情绪标签", "reasoning": "简短分析理由(20字内)"}}
|
||||
|
||||
股评内容:{}
|
||||
"#,
|
||||
content
|
||||
)
|
||||
}
|
||||
|
||||
fn send_request(&self, prompt: &str) -> Result<AnalysisResponse, AnalyzerError> {
|
||||
let url = format!("{}/chat/completions", self.config.base_url.trim_end_matches('/'));
|
||||
|
||||
let body = serde_json::json!({
|
||||
"model": self.config.model,
|
||||
"messages": [
|
||||
{"role": "system", "content": "你是一个专业的股市情绪分析师。请严格按照JSON格式返回结果。"},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
"temperature": 0.3,
|
||||
"max_tokens": 256
|
||||
});
|
||||
|
||||
let response = self.client
|
||||
.post(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", format!("Bearer {}", self.config.api_key))
|
||||
.json(&body)
|
||||
.send()?;
|
||||
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
return Err(AnalyzerError::ApiError(format!("HTTP {}", status)));
|
||||
}
|
||||
|
||||
let result: serde_json::Value = response.json()?;
|
||||
|
||||
if let Some(content) = result.get("choices")
|
||||
.and_then(|c| c.as_array())
|
||||
.and_then(|a| a.first())
|
||||
.and_then(|c| c.get("message"))
|
||||
.and_then(|m| m.get("content"))
|
||||
.and_then(|c| c.as_str())
|
||||
{
|
||||
self.parse_response(content)
|
||||
} else {
|
||||
Err(AnalyzerError::ParseError("Invalid response format".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_response(&self, content: &str) -> Result<AnalysisResponse, AnalyzerError> {
|
||||
let content = content.trim();
|
||||
|
||||
let json_str = if content.contains('{') && content.contains('}') {
|
||||
let start = content.find('{').unwrap_or(0);
|
||||
let end = content.rfind('}').map(|i| i + 1).unwrap_or(content.len());
|
||||
&content[start..end]
|
||||
} else {
|
||||
content
|
||||
};
|
||||
|
||||
let parsed: serde_json::Value = serde_json::from_str(json_str)
|
||||
.map_err(|e| AnalyzerError::ParseError(e.to_string()))?;
|
||||
|
||||
let score = parsed.get("score")
|
||||
.and_then(|s| s.as_i64())
|
||||
.map(|s| s as i32);
|
||||
|
||||
let label = parsed.get("label")
|
||||
.and_then(|l| l.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let reasoning = parsed.get("reasoning")
|
||||
.and_then(|r| r.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
Ok(AnalysisResponse {
|
||||
score,
|
||||
label,
|
||||
reasoning,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_label(&self, score: i32) -> String {
|
||||
match score {
|
||||
0..=29 => "极度悲观".to_string(),
|
||||
30..=39 => "悲观".to_string(),
|
||||
40..=49 => "偏悲观".to_string(),
|
||||
50..=50 => "中性".to_string(),
|
||||
51..=60 => "偏乐观".to_string(),
|
||||
61..=69 => "乐观".to_string(),
|
||||
70..=100 => "极度乐观".to_string(),
|
||||
_ => "无法判断".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_last_result(&self) -> Option<&AnalysisResponse> {
|
||||
self.last_result.as_ref()
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, config: LlmApiConfig) {
|
||||
self.config = config;
|
||||
self.client = Client::builder()
|
||||
.timeout(Duration::from_secs(self.config.timeout))
|
||||
.build()
|
||||
.expect("Failed to create HTTP client");
|
||||
}
|
||||
}
|
||||
207
rust/src/config.rs
Normal file
207
rust/src/config.rs
Normal file
@@ -0,0 +1,207 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use parking_lot::RwLock;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LlmApiConfig {
|
||||
pub base_url: String,
|
||||
pub api_key: String,
|
||||
pub model: String,
|
||||
pub timeout: u64,
|
||||
#[serde(default = "default_retry_times")]
|
||||
pub retry_times: u32,
|
||||
}
|
||||
|
||||
fn default_retry_times() -> u32 { 3 }
|
||||
|
||||
impl Default for LlmApiConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base_url: "https://integrate.api.nvidia.com/v1".to_string(),
|
||||
api_key: "".to_string(),
|
||||
model: "deepseek-ai/deepseek-r1".to_string(),
|
||||
timeout: 120,
|
||||
retry_times: 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SpiderConfig {
|
||||
pub target_url: String,
|
||||
pub xpath: String,
|
||||
#[serde(default = "default_user_agent")]
|
||||
pub user_agent: String,
|
||||
#[serde(default = "default_fetch_interval")]
|
||||
pub fetch_interval: u64,
|
||||
#[serde(default = "default_retry_times")]
|
||||
pub retry_times: u32,
|
||||
#[serde(default = "default_retry_interval")]
|
||||
pub retry_interval: u64,
|
||||
}
|
||||
|
||||
fn default_user_agent() -> String {
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36".to_string()
|
||||
}
|
||||
|
||||
fn default_fetch_interval() -> u64 { 15 }
|
||||
fn default_retry_interval() -> u64 { 5 }
|
||||
|
||||
impl Default for SpiderConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
target_url: "".to_string(),
|
||||
xpath: "".to_string(),
|
||||
user_agent: default_user_agent(),
|
||||
fetch_interval: 15,
|
||||
retry_times: 3,
|
||||
retry_interval: 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UiConfig {
|
||||
#[serde(default = "default_opacity")]
|
||||
pub opacity: f32,
|
||||
#[serde(default = "default_is_on_top")]
|
||||
pub is_on_top: bool,
|
||||
pub thresholds: ThresholdConfig,
|
||||
}
|
||||
|
||||
fn default_opacity() -> f32 { 0.9 }
|
||||
fn default_is_on_top() -> bool { true }
|
||||
|
||||
impl Default for UiConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
opacity: 0.9,
|
||||
is_on_top: true,
|
||||
thresholds: ThresholdConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ThresholdConfig {
|
||||
#[serde(default = "default_cold")]
|
||||
pub cold: i32,
|
||||
#[serde(default = "default_warm")]
|
||||
pub warm: i32,
|
||||
}
|
||||
|
||||
fn default_cold() -> i32 { 30 }
|
||||
fn default_warm() -> i32 { 70 }
|
||||
|
||||
impl Default for ThresholdConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
cold: 30,
|
||||
warm: 70,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DatabaseConfig {
|
||||
#[serde(default = "default_db_path")]
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
fn default_db_path() -> String { "guba.db".to_string() }
|
||||
|
||||
impl Default for DatabaseConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
path: "guba.db".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LoggingConfig {
|
||||
#[serde(default = "default_log_level")]
|
||||
pub level: String,
|
||||
#[serde(default = "default_log_path")]
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
fn default_log_level() -> String { "INFO".to_string() }
|
||||
fn default_log_path() -> String { "guba.log".to_string() }
|
||||
|
||||
impl Default for LoggingConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
level: "INFO".to_string(),
|
||||
path: "guba.log".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub llm_api: LlmApiConfig,
|
||||
pub spider: SpiderConfig,
|
||||
pub ui: UiConfig,
|
||||
pub database: DatabaseConfig,
|
||||
pub logging: LoggingConfig,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
llm_api: LlmApiConfig::default(),
|
||||
spider: SpiderConfig::default(),
|
||||
ui: UiConfig::default(),
|
||||
database: DatabaseConfig::default(),
|
||||
logging: LoggingConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ConfigManager {
|
||||
config: Arc<RwLock<Config>>,
|
||||
config_path: PathBuf,
|
||||
}
|
||||
|
||||
impl ConfigManager {
|
||||
pub fn new(config_file: &str) -> Self {
|
||||
let config_path = PathBuf::from(config_file);
|
||||
let config = if config_path.exists() {
|
||||
match fs::read_to_string(&config_path) {
|
||||
Ok(content) => {
|
||||
serde_json::from_str(&content).unwrap_or_default()
|
||||
}
|
||||
Err(_) => Config::default(),
|
||||
}
|
||||
} else {
|
||||
Config::default()
|
||||
};
|
||||
|
||||
Self {
|
||||
config: Arc::new(RwLock::new(config)),
|
||||
config_path,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self) -> Config {
|
||||
self.config.read().clone()
|
||||
}
|
||||
|
||||
pub fn save(&self) -> Result<(), String> {
|
||||
let config = self.config.read();
|
||||
let content = serde_json::to_string_pretty(&*config)
|
||||
.map_err(|e| e.to_string())?;
|
||||
fs::write(&self.config_path, content).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub fn update<F>(&self, f: F)
|
||||
where
|
||||
F: FnOnce(&mut Config),
|
||||
{
|
||||
let mut config = self.config.write();
|
||||
f(&mut config);
|
||||
}
|
||||
}
|
||||
223
rust/src/database.rs
Normal file
223
rust/src/database.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use rusqlite::{params, Connection, Result as SqlResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Comment {
|
||||
pub id: i64,
|
||||
pub content: String,
|
||||
pub hash: String,
|
||||
pub created_at: String,
|
||||
pub analyzed: bool,
|
||||
pub score: Option<i32>,
|
||||
pub label: Option<String>,
|
||||
pub analyzed_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AnalysisResult {
|
||||
pub id: i64,
|
||||
pub comment_id: i64,
|
||||
pub score: i32,
|
||||
pub label: String,
|
||||
pub analyzed_at: String,
|
||||
}
|
||||
|
||||
pub struct DatabaseManager {
|
||||
conn: Arc<Mutex<Connection>>,
|
||||
}
|
||||
|
||||
impl DatabaseManager {
|
||||
pub fn new(db_path: &str) -> SqlResult<Self> {
|
||||
let conn = Connection::open(Path::new(db_path))?;
|
||||
let manager = Self {
|
||||
conn: Arc::new(Mutex::new(conn)),
|
||||
};
|
||||
manager.init_tables()?;
|
||||
Ok(manager)
|
||||
}
|
||||
|
||||
fn init_tables(&self) -> SqlResult<()> {
|
||||
let conn = self.conn.lock();
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS comments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
content TEXT NOT NULL,
|
||||
hash TEXT NOT NULL UNIQUE,
|
||||
created_at TEXT NOT NULL,
|
||||
analyzed INTEGER DEFAULT 0,
|
||||
score INTEGER,
|
||||
label TEXT,
|
||||
analyzed_at TEXT
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_comments_hash ON comments(hash)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_comments_analyzed ON comments(analyzed)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS analysis_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
comment_id INTEGER NOT NULL,
|
||||
score INTEGER NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
analyzed_at TEXT NOT NULL,
|
||||
FOREIGN KEY (comment_id) REFERENCES comments(id)
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_analysis_comment ON analysis_history(comment_id)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_comment(&self, content: &str) -> SqlResult<Option<i64>> {
|
||||
let hash = self.compute_hash(content);
|
||||
let created_at = Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
||||
|
||||
let conn = self.conn.lock();
|
||||
|
||||
let existing: Option<i64> = conn
|
||||
.query_row(
|
||||
"SELECT id FROM comments WHERE hash = ?",
|
||||
[&hash],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.ok();
|
||||
|
||||
if existing.is_some() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO comments (content, hash, created_at) VALUES (?, ?, ?)",
|
||||
params![content, hash, created_at],
|
||||
)?;
|
||||
|
||||
let id = conn.last_insert_rowid();
|
||||
Ok(Some(id))
|
||||
}
|
||||
|
||||
pub fn add_comments_batch(&self, comments: &[String]) -> SqlResult<Vec<i64>> {
|
||||
let mut new_ids = Vec::new();
|
||||
for content in comments {
|
||||
if let Some(id) = self.add_comment(content)? {
|
||||
new_ids.push(id);
|
||||
}
|
||||
}
|
||||
Ok(new_ids)
|
||||
}
|
||||
|
||||
pub fn get_unanalyzed_comments(&self, limit: usize) -> SqlResult<Vec<Comment>> {
|
||||
let conn = self.conn.lock();
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, content, hash, created_at, analyzed, score, label, analyzed_at
|
||||
FROM comments WHERE analyzed = 0 LIMIT ?",
|
||||
)?;
|
||||
|
||||
let comments = stmt
|
||||
.query_map([limit], |row| {
|
||||
Ok(Comment {
|
||||
id: row.get(0)?,
|
||||
content: row.get(1)?,
|
||||
hash: row.get(2)?,
|
||||
created_at: row.get(3)?,
|
||||
analyzed: row.get::<_, i32>(4)? != 0,
|
||||
score: row.get(5)?,
|
||||
label: row.get(6)?,
|
||||
analyzed_at: row.get(7)?,
|
||||
})
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(comments)
|
||||
}
|
||||
|
||||
pub fn mark_analyzed(&self, comment_id: i64, score: i32, label: &str) -> SqlResult<()> {
|
||||
let analyzed_at = Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
||||
|
||||
let conn = self.conn.lock();
|
||||
|
||||
conn.execute(
|
||||
"UPDATE comments SET analyzed = 1, score = ?, label = ?, analyzed_at = ? WHERE id = ?",
|
||||
params![score, label, analyzed_at, comment_id],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO analysis_history (comment_id, score, label, analyzed_at) VALUES (?, ?, ?, ?)",
|
||||
params![comment_id, score, label, analyzed_at],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_all_scores(&self, limit: usize) -> SqlResult<Vec<i32>> {
|
||||
let conn = self.conn.lock();
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT score FROM comments WHERE score IS NOT NULL ORDER BY analyzed_at DESC LIMIT ?",
|
||||
)?;
|
||||
|
||||
let scores = stmt
|
||||
.query_map([limit], |row| row.get(0))?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(scores)
|
||||
}
|
||||
|
||||
pub fn get_statistics(&self) -> SqlResult<Statistics> {
|
||||
let conn = self.conn.lock();
|
||||
|
||||
let total: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM comments",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
let analyzed: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM comments WHERE analyzed = 1",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
let avg_score: Option<f64> = conn.query_row(
|
||||
"SELECT AVG(score) FROM comments WHERE score IS NOT NULL",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
).ok();
|
||||
|
||||
Ok(Statistics {
|
||||
total_comments: total as usize,
|
||||
analyzed_comments: analyzed as usize,
|
||||
average_score: avg_score.map(|s| s as i32),
|
||||
})
|
||||
}
|
||||
|
||||
fn compute_hash(&self, content: &str) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(content.as_bytes());
|
||||
hex::encode(hasher.finalize())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Statistics {
|
||||
pub total_comments: usize,
|
||||
pub analyzed_comments: usize,
|
||||
pub average_score: Option<i32>,
|
||||
}
|
||||
9
rust/src/lib.rs
Normal file
9
rust/src/lib.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
pub mod config;
|
||||
pub mod database;
|
||||
pub mod spider;
|
||||
pub mod analyzer;
|
||||
|
||||
pub use config::ConfigManager;
|
||||
pub use database::DatabaseManager;
|
||||
pub use spider::SpiderManager;
|
||||
pub use analyzer::LLMAnalyzer;
|
||||
276
rust/src/main.rs
Normal file
276
rust/src/main.rs
Normal file
@@ -0,0 +1,276 @@
|
||||
mod config;
|
||||
mod database;
|
||||
mod spider;
|
||||
mod analyzer;
|
||||
mod ui;
|
||||
|
||||
use config::{Config, ConfigManager};
|
||||
use database::DatabaseManager;
|
||||
use spider::SpiderManager;
|
||||
use analyzer::LLMAnalyzer;
|
||||
use ui::{AppState, draw_indicator, draw_waveform, get_score_label};
|
||||
|
||||
use eframe::egui;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
struct GubaApp {
|
||||
config_manager: ConfigManager,
|
||||
db: DatabaseManager,
|
||||
spider: SpiderManager,
|
||||
analyzer: LLMAnalyzer,
|
||||
state: AppState,
|
||||
stock_data: Arc<parking_lot::Mutex<Vec<(String, f64)>>>,
|
||||
config_open: bool,
|
||||
}
|
||||
|
||||
impl GubaApp {
|
||||
fn new(cc: &eframe::CreationContext<'_>, config_manager: ConfigManager) -> Self {
|
||||
let config = config_manager.get();
|
||||
|
||||
let db = DatabaseManager::new(&config.database.path)
|
||||
.expect("Failed to initialize database");
|
||||
|
||||
let spider = SpiderManager::new(config.spider.clone());
|
||||
|
||||
let mut analyzer = LLMAnalyzer::new(config.llm_api.clone());
|
||||
|
||||
let state = AppState::new();
|
||||
let stock_data = Arc::new(parking_lot::Mutex::new(Vec::new()));
|
||||
|
||||
let db_clone = Arc::new(database::DatabaseManager::new(&config.database.path)
|
||||
.expect("Failed to create database clone"));
|
||||
let spider_clone = Arc::new(spider::SpiderManager::new(config.spider.clone()));
|
||||
let mut analyzer_clone = analyzer::LLMAnalyzer::new(config.llm_api.clone());
|
||||
|
||||
let state_clone = Arc::new(ui::AppState::new());
|
||||
let stock_data_clone = stock_data.clone();
|
||||
|
||||
thread::spawn(move || {
|
||||
run_background_task(db_clone, spider_clone, &mut analyzer_clone, state_clone, stock_data_clone);
|
||||
});
|
||||
|
||||
Self {
|
||||
config_manager,
|
||||
db,
|
||||
spider,
|
||||
analyzer,
|
||||
state,
|
||||
stock_data,
|
||||
config_open: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_background_task(
|
||||
db: Arc<DatabaseManager>,
|
||||
spider: Arc<SpiderManager>,
|
||||
analyzer: &mut LLMAnalyzer,
|
||||
state: Arc<ui::AppState>,
|
||||
stock_data: Arc<parking_lot::Mutex<Vec<(String, f64)>>>,
|
||||
) {
|
||||
let mut no_content_count = 0i32;
|
||||
let mut fetch_interval = 15u64;
|
||||
|
||||
loop {
|
||||
if !state.running.load(Ordering::SeqCst) {
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
continue;
|
||||
}
|
||||
|
||||
{
|
||||
let mut status = state.status_text.lock();
|
||||
*status = "正在爬取评论...".to_string();
|
||||
}
|
||||
|
||||
let comments = match spider.fetch() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
let mut status = state.status_text.lock();
|
||||
*status = format!("爬取失败: {}", e);
|
||||
thread::sleep(Duration::from_secs(5));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if comments.is_empty() {
|
||||
no_content_count += 1;
|
||||
let interval = fetch_interval * (1 + no_content_count.min(4) as u64);
|
||||
let mut status = state.status_text.lock();
|
||||
*status = format!("无新内容,{}秒后重试", interval);
|
||||
thread::sleep(Duration::from_secs(interval));
|
||||
continue;
|
||||
}
|
||||
|
||||
no_content_count = 0;
|
||||
state.fetch_count.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
{
|
||||
let mut status = state.status_text.lock();
|
||||
*status = format!("获取到 {} 条评论", comments.len());
|
||||
}
|
||||
|
||||
match db.add_comments_batch(&comments) {
|
||||
Ok(new_ids) if !new_ids.is_empty() => {
|
||||
let thresholds = {
|
||||
let config = ConfigManager::new("config.json");
|
||||
config.get().ui.thresholds
|
||||
};
|
||||
|
||||
for id in &new_ids {
|
||||
if let Ok(unanalyzed) = db.get_unanalyzed_comments(1) {
|
||||
if let Some(comment) = unanalyzed.first() {
|
||||
match analyzer.analyze(&comment.content) {
|
||||
Ok((score, label)) => {
|
||||
let _ = db.mark_analyzed(comment.id, score, &label);
|
||||
state.analysis_count.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
let scores = db.get_all_scores(100).unwrap_or_default();
|
||||
if !scores.is_empty() {
|
||||
let avg: i32 = scores.iter().sum::<i32>() / scores.len() as i32;
|
||||
state.current_score.store(avg, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
let mut status = state.status_text.lock();
|
||||
*status = format!("分析完成: {}分 - {}", score, label);
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = db.mark_analyzed(comment.id, 50, "分析异常");
|
||||
let mut status = state.status_text.lock();
|
||||
*status = format!("分析失败: {}", e);
|
||||
}
|
||||
}
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Ok(data) = spider.fetch_sse_stock_data() {
|
||||
if data.value > 0.0 {
|
||||
let mut stocks = stock_data.lock();
|
||||
stocks.push((data.time.clone(), data.value));
|
||||
if stocks.len() > 100 {
|
||||
stocks.remove(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let interval = fetch_interval * (1 + no_content_count.min(4) as u64);
|
||||
thread::sleep(Duration::from_secs(interval));
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for GubaApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.heading("股吧人气指示器");
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button(if self.state.running.load(Ordering::SeqCst) { "停止" } else { "开始" }).clicked() {
|
||||
let running = self.state.running.load(Ordering::SeqCst);
|
||||
self.state.running.store(!running, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if ui.button("刷新").clicked() {
|
||||
self.state.no_content_count.store(0, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if ui.button("配置").clicked() {
|
||||
self.config_open = true;
|
||||
}
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
let config = self.config_manager.get();
|
||||
let score = self.state.current_score.load(Ordering::SeqCst);
|
||||
let cold = config.ui.thresholds.cold;
|
||||
let warm = config.ui.thresholds.warm;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("当前情绪:");
|
||||
draw_indicator(ui, score, cold, warm);
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.label("上证指数走势:");
|
||||
let stock_data = self.stock_data.lock();
|
||||
let data: Vec<(String, f64)> = stock_data.clone();
|
||||
drop(stock_data);
|
||||
|
||||
egui::ScrollArea::vertical().max_height(200.0).show(ui, |ui| {
|
||||
draw_waveform(ui, &data, ui.available_width(), 150.0);
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("状态: ");
|
||||
let status = self.state.status_text.lock();
|
||||
ui.label(status.clone());
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(format!("爬取次数: {}", self.state.fetch_count.load(Ordering::SeqCst)));
|
||||
ui.label(format!("分析次数: {}", self.state.analysis_count.load(Ordering::SeqCst)));
|
||||
});
|
||||
});
|
||||
|
||||
if self.config_open {
|
||||
egui::Window::new("配置").open(&mut self.config_open).show(ctx, |ui| {
|
||||
let config = self.config_manager.get();
|
||||
|
||||
ui.group(|ui| {
|
||||
ui.label("API配置:");
|
||||
ui.text_edit_singleline(&mut self.config_manager.get().llm_api.api_key.clone());
|
||||
});
|
||||
|
||||
ui.group(|ui| {
|
||||
ui.label("爬虫配置:");
|
||||
ui.text_edit_singleline(&mut self.config_manager.get().spider.target_url.clone());
|
||||
});
|
||||
|
||||
ui.group(|ui| {
|
||||
ui.label("阈值设置:");
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("冷阈值:");
|
||||
ui.add(egui::DragValue::new(&mut self.config_manager.get().ui.thresholds.cold).range(0..=100));
|
||||
ui.label("热阈值:");
|
||||
ui.add(egui::DragValue::new(&mut self.config_manager.get().ui.thresholds.warm).range(0..=100));
|
||||
});
|
||||
});
|
||||
|
||||
if ui.button("保存").clicked() {
|
||||
let _ = self.config_manager.save();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> eframe::Result<()> {
|
||||
let config_manager = ConfigManager::new("config.json");
|
||||
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
|
||||
.init();
|
||||
|
||||
log::info!("股吧人气指示器启动");
|
||||
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_inner_size([400.0, 600.0])
|
||||
.with_min_inner_size([300.0, 400.0])
|
||||
.with_resizable(true),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
eframe::run_native(
|
||||
"股吧人气指示器",
|
||||
options,
|
||||
Box::new(|cc| Ok(Box::new(GubaApp::new(cc, config_manager)))),
|
||||
)
|
||||
}
|
||||
120
rust/src/spider.rs
Normal file
120
rust/src/spider.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
use crate::config::SpiderConfig;
|
||||
use regex::Regex;
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SpiderError {
|
||||
#[error("Request failed: {0}")]
|
||||
RequestError(#[from] reqwest::Error),
|
||||
#[error("Parse error: {0}")]
|
||||
ParseError(String),
|
||||
#[error("No comments found")]
|
||||
NoComments,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StockData {
|
||||
pub time: String,
|
||||
pub value: f64,
|
||||
}
|
||||
|
||||
pub struct SpiderManager {
|
||||
config: SpiderConfig,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl SpiderManager {
|
||||
pub fn new(config: SpiderConfig) -> Self {
|
||||
let client = Client::builder()
|
||||
.user_agent(&config.user_agent)
|
||||
.timeout(Duration::from_secs(30))
|
||||
.build()
|
||||
.expect("Failed to create HTTP client");
|
||||
|
||||
Self { config, client }
|
||||
}
|
||||
|
||||
pub fn fetch(&self) -> Result<Vec<String>, SpiderError> {
|
||||
if self.config.target_url.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let response = self.client.get(&self.config.target_url).send()?;
|
||||
let html = response.text()?;
|
||||
|
||||
self.parse_comments(&html)
|
||||
}
|
||||
|
||||
fn parse_comments(&self, html: &str) -> Result<Vec<String>, SpiderError> {
|
||||
if self.config.xpath.is_empty() {
|
||||
return Err(SpiderError::ParseError("XPath is empty".to_string()));
|
||||
}
|
||||
|
||||
let mut comments = Vec::new();
|
||||
|
||||
if let Ok(re) = Regex::new(r#"<[^>]*>([^<]+)</[^>]*>"#) {
|
||||
for cap in re.captures_iter(html) {
|
||||
if let Some(content) = cap.get(1) {
|
||||
let text = content.as_str().trim().to_string();
|
||||
if !text.is_empty() && text.len() > 5 {
|
||||
comments.push(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if comments.is_empty() {
|
||||
if let Ok(re) = Regex::new(r#"[\u4e00-\u9fa5]{4,}"#) {
|
||||
for mat in re.find_iter(html) {
|
||||
let text = mat.as_str().to_string();
|
||||
if !comments.contains(&text) {
|
||||
comments.push(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(comments)
|
||||
}
|
||||
|
||||
pub fn fetch_sse_stock_data(&self) -> Result<StockData, SpiderError> {
|
||||
let url = "https://hq.sinajs.cn/list=s_sh000001";
|
||||
|
||||
let response = self.client.get(url)
|
||||
.header("Referer", "https://finance.sina.com.cn/")
|
||||
.send()?;
|
||||
|
||||
let text = response.text()?;
|
||||
|
||||
if let Ok(re) = Regex::new(r#"="([^"]+)""#) {
|
||||
if let Some(cap) = re.captures(&text) {
|
||||
if let Some(data) = cap.get(1) {
|
||||
let parts: Vec<&str> = data.as_str().split(',').collect();
|
||||
if parts.len() >= 32 {
|
||||
let value: f64 = parts[1].parse().unwrap_or(0.0);
|
||||
let time = parts[31].to_string();
|
||||
return Ok(StockData { time, value });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let now = chrono::Local::now();
|
||||
Ok(StockData {
|
||||
time: now.format("%H:%M:%S").to_string(),
|
||||
value: 0.0,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, config: SpiderConfig) {
|
||||
self.config = config;
|
||||
self.client = Client::builder()
|
||||
.user_agent(&self.config.user_agent)
|
||||
.timeout(Duration::from_secs(30))
|
||||
.build()
|
||||
.expect("Failed to create HTTP client");
|
||||
}
|
||||
}
|
||||
125
rust/src/ui.rs
Normal file
125
rust/src/ui.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use egui::{Color32, RichText, Stroke, Vec2};
|
||||
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct AppState {
|
||||
pub running: Arc<AtomicBool>,
|
||||
pub current_score: Arc<AtomicI32>,
|
||||
pub status_text: Arc<parking_lot::Mutex<String>>,
|
||||
pub fetch_count: Arc<AtomicI32>,
|
||||
pub analysis_count: Arc<AtomicI32>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
running: Arc::new(AtomicBool::new(false)),
|
||||
current_score: Arc::new(AtomicI32::new(50)),
|
||||
status_text: Arc::new(parking_lot::Mutex::new("就绪".to_string())),
|
||||
fetch_count: Arc::new(AtomicI32::new(0)),
|
||||
analysis_count: Arc::new(AtomicI32::new(0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AppState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_score_color(score: i32) -> Color32 {
|
||||
match score {
|
||||
0..=30 => Color32::from_rgb(0, 100, 200),
|
||||
31..=50 => Color32::from_rgb(100, 200, 100),
|
||||
51..=70 => Color32::from_rgb(255, 200, 0),
|
||||
71..=100 => Color32::from_rgb(255, 50, 50),
|
||||
_ => Color32::GRAY,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_score_label(score: i32, cold: i32, warm: i32) -> &'static str {
|
||||
if score < cold {
|
||||
"看跌"
|
||||
} else if score > warm {
|
||||
"看涨"
|
||||
} else {
|
||||
"中性"
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_indicator(ui: &mut egui::Ui, score: i32, cold: i32, warm: i32) {
|
||||
let color = get_score_color(score);
|
||||
let label = get_score_label(score, cold, warm);
|
||||
let size = Vec2::new(120.0, 120.0);
|
||||
|
||||
let (rect, _response) = ui.allocate_exact_size(size, egui::Sense::hover());
|
||||
|
||||
let painter = ui.painter();
|
||||
|
||||
painter.circle_filled(rect.center(), 55.0, Color32::from_rgba_unmultiplied(30, 30, 30, 200));
|
||||
painter.circle_stroke(rect.center(), 55.0, Stroke::new(3.0, color));
|
||||
|
||||
let inner_radius = 40.0;
|
||||
let angle = (score as f32 / 100.0) * std::f32::consts::TAU - std::f32::consts::FRAC_PI_2;
|
||||
let indicator_pos = rect.center() + Vec2::new(
|
||||
angle.cos() * inner_radius,
|
||||
angle.sin() * inner_radius,
|
||||
);
|
||||
painter.circle_filled(indicator_pos, 8.0, color);
|
||||
|
||||
let text = RichText::new(format!("{}", score))
|
||||
.heading()
|
||||
.size(36.0)
|
||||
.color(color);
|
||||
painter.text(
|
||||
rect.center() - Vec2::new(0.0, 10.0),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
text,
|
||||
);
|
||||
|
||||
let label_text = RichText::new(label).size(16.0).color(Color32::WHITE);
|
||||
painter.text(
|
||||
rect.center() + Vec2::new(0.0, 25.0),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
label_text,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn draw_waveform(ui: &mut egui::Ui, data: &[(String, f64)], width: f32, height: f32) {
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
let painter = ui.painter();
|
||||
|
||||
painter.rect_filled(rect, 0.0, Color32::from_rgba_unmultiplied(20, 20, 30, 180));
|
||||
painter.rect_stroke(rect, 0.0, Stroke::new(1.0, Color32::GRAY));
|
||||
|
||||
if data.is_empty() {
|
||||
let text = RichText::new("暂无数据").small().color(Color32::GRAY);
|
||||
painter.text(rect.center(), egui::Align2::CENTER_CENTER, text);
|
||||
return;
|
||||
}
|
||||
|
||||
let values: Vec<f64> = data.iter().map(|(_, v)| *v).collect();
|
||||
let min_val = values.iter().cloned().fold(f64::INFINITY, f64::min);
|
||||
let max_val = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
|
||||
let range = if (max_val - min_val).abs() < 0.01 { 1.0 } else { max_val - min_val };
|
||||
|
||||
let padding = 10.0;
|
||||
let draw_width = rect.width() - padding * 2.0;
|
||||
let draw_height = rect.height() - padding * 2.0;
|
||||
|
||||
let step_x = if data.len() > 1 { draw_width / (data.len() - 1) as f32 } else { 0.0 };
|
||||
|
||||
for i in 0..data.len().saturating_sub(1) {
|
||||
let x1 = rect.min.x + padding + (i as f32) * step_x;
|
||||
let x2 = rect.min.x + padding + ((i + 1) as f32) * step_x;
|
||||
|
||||
let y1 = rect.max.y - padding - ((values[i] - min_val) / range * draw_height as f64) as f32;
|
||||
let y2 = rect.max.y - padding - ((values[i + 1] - min_val) / range * draw_height as f64) as f32;
|
||||
|
||||
painter.line_segment(
|
||||
[egui::pos2(x1, y1), egui::pos2(x2, y2)],
|
||||
Stroke::new(2.0, Color32::from_rgb(0, 150, 255)),
|
||||
);
|
||||
}
|
||||
}
|
||||
1
rust/target/.rustc_info.json
Normal file
1
rust/target/.rustc_info.json
Normal file
@@ -0,0 +1 @@
|
||||
{"rustc_fingerprint":15274735761647741548,"outputs":{"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___.exe\nlib___.rlib\n___.dll\n___.dll\n___.lib\n___.dll\nC:\\Users\\dxzq\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc\npacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"msvc\"\ntarget_family=\"windows\"\ntarget_feature=\"cmpxchg16b\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_feature=\"sse3\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"windows\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"pc\"\nwindows\n","stderr":""},"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.93.1 (01f6ddf75 2026-02-11)\nbinary: rustc\ncommit-hash: 01f6ddf7588f42ae2d7eb0a2f21d44e8e96674cf\ncommit-date: 2026-02-11\nhost: x86_64-pc-windows-msvc\nrelease: 1.93.1\nLLVM version: 21.1.8\n","stderr":""}},"successes":{}}
|
||||
3
rust/target/CACHEDIR.TAG
Normal file
3
rust/target/CACHEDIR.TAG
Normal file
@@ -0,0 +1,3 @@
|
||||
Signature: 8a477f597d28d172789f06886806bc55
|
||||
# This file is a cache directory tag created by cargo.
|
||||
# For information about cache directory tags see https://bford.info/cachedir/
|
||||
0
rust/target/release/.cargo-lock
Normal file
0
rust/target/release/.cargo-lock
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
b60a5906b2002723
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":8323788817864214825,"features":"[]","declared_features":"[\"core\", \"rustc-dep-of-std\"]","target":13840298032947503755,"profile":3660878992414372870,"path":5157919149803396374,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release\\.fingerprint\\cfg-if-d2d7e488c9dc3d69\\dep-lib-cfg_if","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1,5 @@
|
||||
{"$message_type":"diagnostic","message":"linker `link.exe` not found","code":null,"level":"error","spans":[],"children":[{"message":"program not found","code":null,"level":"note","spans":[],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[91merror\u001b[0m\u001b[1m\u001b[97m: linker `link.exe` not found\u001b[0m\n \u001b[1m\u001b[96m|\u001b[0m\n \u001b[1m\u001b[96m= \u001b[0m\u001b[1m\u001b[97mnote\u001b[0m: program not found\n\n"}
|
||||
{"$message_type":"diagnostic","message":"the msvc targets depend on the msvc linker but `link.exe` was not found","code":null,"level":"note","spans":[],"children":[],"rendered":"\u001b[1m\u001b[92mnote\u001b[0m\u001b[1m\u001b[97m: the msvc targets depend on the msvc linker but `link.exe` was not found\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"please ensure that Visual Studio 2017 or later, or Build Tools for Visual Studio were installed with the Visual C++ option.","code":null,"level":"note","spans":[],"children":[],"rendered":"\u001b[1m\u001b[92mnote\u001b[0m\u001b[1m\u001b[97m: please ensure that Visual Studio 2017 or later, or Build Tools for Visual Studio were installed with the Visual C++ option.\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"VS Code is a different product, and is not sufficient.","code":null,"level":"note","spans":[],"children":[],"rendered":"\u001b[1m\u001b[92mnote\u001b[0m\u001b[1m\u001b[97m: VS Code is a different product, and is not sufficient.\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"aborting due to 1 previous error","code":null,"level":"error","spans":[],"children":[],"rendered":"\u001b[1m\u001b[91merror\u001b[0m\u001b[1m\u001b[97m: aborting due to 1 previous error\u001b[0m\n\n"}
|
||||
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1,5 @@
|
||||
{"$message_type":"diagnostic","message":"linker `link.exe` not found","code":null,"level":"error","spans":[],"children":[{"message":"program not found","code":null,"level":"note","spans":[],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[91merror\u001b[0m\u001b[1m\u001b[97m: linker `link.exe` not found\u001b[0m\n \u001b[1m\u001b[96m|\u001b[0m\n \u001b[1m\u001b[96m= \u001b[0m\u001b[1m\u001b[97mnote\u001b[0m: program not found\n\n"}
|
||||
{"$message_type":"diagnostic","message":"the msvc targets depend on the msvc linker but `link.exe` was not found","code":null,"level":"note","spans":[],"children":[],"rendered":"\u001b[1m\u001b[92mnote\u001b[0m\u001b[1m\u001b[97m: the msvc targets depend on the msvc linker but `link.exe` was not found\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"please ensure that Visual Studio 2017 or later, or Build Tools for Visual Studio were installed with the Visual C++ option.","code":null,"level":"note","spans":[],"children":[],"rendered":"\u001b[1m\u001b[92mnote\u001b[0m\u001b[1m\u001b[97m: please ensure that Visual Studio 2017 or later, or Build Tools for Visual Studio were installed with the Visual C++ option.\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"VS Code is a different product, and is not sufficient.","code":null,"level":"note","spans":[],"children":[],"rendered":"\u001b[1m\u001b[92mnote\u001b[0m\u001b[1m\u001b[97m: VS Code is a different product, and is not sufficient.\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"aborting due to 1 previous error","code":null,"level":"error","spans":[],"children":[],"rendered":"\u001b[1m\u001b[91merror\u001b[0m\u001b[1m\u001b[97m: aborting due to 1 previous error\u001b[0m\n\n"}
|
||||
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1,5 @@
|
||||
{"$message_type":"diagnostic","message":"linker `link.exe` not found","code":null,"level":"error","spans":[],"children":[{"message":"program not found","code":null,"level":"note","spans":[],"children":[],"rendered":null}],"rendered":"\u001b[1m\u001b[91merror\u001b[0m\u001b[1m\u001b[97m: linker `link.exe` not found\u001b[0m\n \u001b[1m\u001b[96m|\u001b[0m\n \u001b[1m\u001b[96m= \u001b[0m\u001b[1m\u001b[97mnote\u001b[0m: program not found\n\n"}
|
||||
{"$message_type":"diagnostic","message":"the msvc targets depend on the msvc linker but `link.exe` was not found","code":null,"level":"note","spans":[],"children":[],"rendered":"\u001b[1m\u001b[92mnote\u001b[0m\u001b[1m\u001b[97m: the msvc targets depend on the msvc linker but `link.exe` was not found\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"please ensure that Visual Studio 2017 or later, or Build Tools for Visual Studio were installed with the Visual C++ option.","code":null,"level":"note","spans":[],"children":[],"rendered":"\u001b[1m\u001b[92mnote\u001b[0m\u001b[1m\u001b[97m: please ensure that Visual Studio 2017 or later, or Build Tools for Visual Studio were installed with the Visual C++ option.\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"VS Code is a different product, and is not sufficient.","code":null,"level":"note","spans":[],"children":[],"rendered":"\u001b[1m\u001b[92mnote\u001b[0m\u001b[1m\u001b[97m: VS Code is a different product, and is not sufficient.\u001b[0m\n\n"}
|
||||
{"$message_type":"diagnostic","message":"aborting due to 1 previous error","code":null,"level":"error","spans":[],"children":[],"rendered":"\u001b[1m\u001b[91merror\u001b[0m\u001b[1m\u001b[97m: aborting due to 1 previous error\u001b[0m\n\n"}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
ca12b79f6a1a1d70
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":8323788817864214825,"features":"[]","declared_features":"[]","target":14045917370260632744,"profile":17984201634715228204,"path":9962931808668317007,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release\\.fingerprint\\unicode-ident-0082d2f4b99fbc4a\\dep-lib-unicode_ident","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
f059b888e2d2ac91
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":8323788817864214825,"features":"[]","declared_features":"[]","target":2558631941022679061,"profile":310928410307151240,"path":3878061000663426425,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"release\\.fingerprint\\windows-link-6e2139e2f73745b5\\dep-lib-windows_link","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
7
rust/target/release/deps/cfg_if-d2d7e488c9dc3d69.d
Normal file
7
rust/target/release/deps/cfg_if-d2d7e488c9dc3d69.d
Normal file
@@ -0,0 +1,7 @@
|
||||
H:\学习资料\自用的小工具\guba\rust\target\release\deps\cfg_if-d2d7e488c9dc3d69.d: C:\Users\dxzq\.cargo\registry\src\mirrors.ustc.edu.cn-38d0e5eb5da2abae\cfg-if-1.0.4\src\lib.rs
|
||||
|
||||
H:\学习资料\自用的小工具\guba\rust\target\release\deps\libcfg_if-d2d7e488c9dc3d69.rlib: C:\Users\dxzq\.cargo\registry\src\mirrors.ustc.edu.cn-38d0e5eb5da2abae\cfg-if-1.0.4\src\lib.rs
|
||||
|
||||
H:\学习资料\自用的小工具\guba\rust\target\release\deps\libcfg_if-d2d7e488c9dc3d69.rmeta: C:\Users\dxzq\.cargo\registry\src\mirrors.ustc.edu.cn-38d0e5eb5da2abae\cfg-if-1.0.4\src\lib.rs
|
||||
|
||||
C:\Users\dxzq\.cargo\registry\src\mirrors.ustc.edu.cn-38d0e5eb5da2abae\cfg-if-1.0.4\src\lib.rs:
|
||||
BIN
rust/target/release/deps/libcfg_if-d2d7e488c9dc3d69.rlib
Normal file
BIN
rust/target/release/deps/libcfg_if-d2d7e488c9dc3d69.rlib
Normal file
Binary file not shown.
BIN
rust/target/release/deps/libcfg_if-d2d7e488c9dc3d69.rmeta
Normal file
BIN
rust/target/release/deps/libcfg_if-d2d7e488c9dc3d69.rmeta
Normal file
Binary file not shown.
BIN
rust/target/release/deps/libunicode_ident-0082d2f4b99fbc4a.rlib
Normal file
BIN
rust/target/release/deps/libunicode_ident-0082d2f4b99fbc4a.rlib
Normal file
Binary file not shown.
BIN
rust/target/release/deps/libunicode_ident-0082d2f4b99fbc4a.rmeta
Normal file
BIN
rust/target/release/deps/libunicode_ident-0082d2f4b99fbc4a.rmeta
Normal file
Binary file not shown.
BIN
rust/target/release/deps/libwindows_link-6e2139e2f73745b5.rlib
Normal file
BIN
rust/target/release/deps/libwindows_link-6e2139e2f73745b5.rlib
Normal file
Binary file not shown.
BIN
rust/target/release/deps/libwindows_link-6e2139e2f73745b5.rmeta
Normal file
BIN
rust/target/release/deps/libwindows_link-6e2139e2f73745b5.rmeta
Normal file
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
H:\学习资料\自用的小工具\guba\rust\target\release\deps\unicode_ident-0082d2f4b99fbc4a.d: C:\Users\dxzq\.cargo\registry\src\mirrors.ustc.edu.cn-38d0e5eb5da2abae\unicode-ident-1.0.24\src\lib.rs C:\Users\dxzq\.cargo\registry\src\mirrors.ustc.edu.cn-38d0e5eb5da2abae\unicode-ident-1.0.24\src\tables.rs
|
||||
|
||||
H:\学习资料\自用的小工具\guba\rust\target\release\deps\libunicode_ident-0082d2f4b99fbc4a.rlib: C:\Users\dxzq\.cargo\registry\src\mirrors.ustc.edu.cn-38d0e5eb5da2abae\unicode-ident-1.0.24\src\lib.rs C:\Users\dxzq\.cargo\registry\src\mirrors.ustc.edu.cn-38d0e5eb5da2abae\unicode-ident-1.0.24\src\tables.rs
|
||||
|
||||
H:\学习资料\自用的小工具\guba\rust\target\release\deps\libunicode_ident-0082d2f4b99fbc4a.rmeta: C:\Users\dxzq\.cargo\registry\src\mirrors.ustc.edu.cn-38d0e5eb5da2abae\unicode-ident-1.0.24\src\lib.rs C:\Users\dxzq\.cargo\registry\src\mirrors.ustc.edu.cn-38d0e5eb5da2abae\unicode-ident-1.0.24\src\tables.rs
|
||||
|
||||
C:\Users\dxzq\.cargo\registry\src\mirrors.ustc.edu.cn-38d0e5eb5da2abae\unicode-ident-1.0.24\src\lib.rs:
|
||||
C:\Users\dxzq\.cargo\registry\src\mirrors.ustc.edu.cn-38d0e5eb5da2abae\unicode-ident-1.0.24\src\tables.rs:
|
||||
8
rust/target/release/deps/windows_link-6e2139e2f73745b5.d
Normal file
8
rust/target/release/deps/windows_link-6e2139e2f73745b5.d
Normal file
@@ -0,0 +1,8 @@
|
||||
H:\学习资料\自用的小工具\guba\rust\target\release\deps\windows_link-6e2139e2f73745b5.d: C:\Users\dxzq\.cargo\registry\src\mirrors.ustc.edu.cn-38d0e5eb5da2abae\windows-link-0.2.1\src\lib.rs C:\Users\dxzq\.cargo\registry\src\mirrors.ustc.edu.cn-38d0e5eb5da2abae\windows-link-0.2.1\src\../readme.md
|
||||
|
||||
H:\学习资料\自用的小工具\guba\rust\target\release\deps\libwindows_link-6e2139e2f73745b5.rlib: C:\Users\dxzq\.cargo\registry\src\mirrors.ustc.edu.cn-38d0e5eb5da2abae\windows-link-0.2.1\src\lib.rs C:\Users\dxzq\.cargo\registry\src\mirrors.ustc.edu.cn-38d0e5eb5da2abae\windows-link-0.2.1\src\../readme.md
|
||||
|
||||
H:\学习资料\自用的小工具\guba\rust\target\release\deps\libwindows_link-6e2139e2f73745b5.rmeta: C:\Users\dxzq\.cargo\registry\src\mirrors.ustc.edu.cn-38d0e5eb5da2abae\windows-link-0.2.1\src\lib.rs C:\Users\dxzq\.cargo\registry\src\mirrors.ustc.edu.cn-38d0e5eb5da2abae\windows-link-0.2.1\src\../readme.md
|
||||
|
||||
C:\Users\dxzq\.cargo\registry\src\mirrors.ustc.edu.cn-38d0e5eb5da2abae\windows-link-0.2.1\src\lib.rs:
|
||||
C:\Users\dxzq\.cargo\registry\src\mirrors.ustc.edu.cn-38d0e5eb5da2abae\windows-link-0.2.1\src\../readme.md:
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 39 KiB |
Reference in New Issue
Block a user