1use std::collections::VecDeque;
2use std::fs;
3use std::fs::File;
4use std::io::{Cursor, Read, Write};
5use std::path::PathBuf;
6use std::process::Stdio;
7use std::sync::{Arc, LazyLock};
8use std::time::Duration;
9use kovi::{Message, PluginBuilder as plugin, PluginBuilder};
10use kovi::log::{error, info};
11use kovi::tokio::time::{interval, timeout};
12use kovi_onebot::{EventRegistrar, MessageRegistrar, MsgEvent};
13use reqwest::{Client};
14use reqwest::header::HeaderMap;
15use serde::{Deserialize, Serialize};
16use tokio::process::{Child, Command};
17use dashmap::DashMap;
18use tokio::io::{AsyncBufReadExt, BufReader};
19use tokio::sync::Mutex;
20
21static STATUS: LazyLock<DashMap<String, Process>> = LazyLock::new(|| DashMap::new());
22
23struct Process {
24 child: Child,
25 output: Arc<Mutex<VecDeque<String>>>,
26}
27
28#[kovi::plugin]
29async fn main() {
30 let bot = PluginBuilder::get_runtime_bot();
31 let data_path = bot.get_data_path();
32 let path = check_exists(data_path).await;
33 plugin::on_msg(move |event| {
34 let bot = bot.clone();
35 let path = path.clone();
36 async move {
37 let text = event.borrow_text().unwrap_or("");
38 if text.starts_with("登录农场") {
39 let data = get_login_url(event.clone(), path).await;
40 let msg = Message::new()
41 .add_text(data.url);
42 event.reply(msg);
43 } else if text.starts_with("农场状态") {
44 let uid = event.user_id.to_string();
45 if STATUS.contains_key(&uid) {
46 let output = get_output(uid).await;
47 let mut text = "农场状态:\n".to_string();
48 for i in output {
49 text.push_str(format!("{}\n", i).as_str());
50 }
51 let msg = Message::new()
52 .add_text(text);
53 event.reply(msg);
54 } else {
55 let msg = Message::new()
56 .add_text("您还未登录,请先登录后操作");
57 event.reply(msg);
58 }
59 } else if text.starts_with("获取QQ名") {
60 let msg = Message::new()
61 .add_text(format!("{}", event.get_sender_nickname()));
62 event.reply(msg);
63 } else if text.starts_with("农场在线数") {
64 let msg = Message::new()
65 .add_text(format!("当前脚本在线数:{}", STATUS.len()));
66 event.reply(msg);
67 } else if text.starts_with("退出农场") {
68 let uid = event.user_id.to_string();
69 if let Some(mut entry) = STATUS.get_mut(&uid) {
70 entry.child.kill().await.ok();
71 drop(entry);
72 STATUS.remove(&uid);
73 event.reply("已退出农场,脚本已停止运行");
74 } else {
75 event.reply("您当前没有正在运行的农场脚本");
76 }
77 } else if text.starts_with("农场帮助") {
78 let help_text = r#"🌾 农场插件帮助
79
80可用命令:
81• 登录农场 - 获取登录链接,登录后自动启动农场脚本
82• 农场状态 - 查看当前脚本运行状态及最近日志
83• 农场在线数 - 查看当前脚本在线数量
84• 退出农场 - 停止当前运行的农场脚本
85• 农场帮助 - 显示此帮助信息"#;
86 let msg = Message::new()
87 .add_text(help_text);
88 event.reply(msg);
89 }
90 }
91 });
92}
93
94async fn get_output(user_id: String) -> VecDeque<String> {
95 if let Some(entry) = STATUS.get(&user_id) {
96 entry.output.lock().await.clone()
97 } else {
98 VecDeque::new()
99 }
100}
101
102async fn check_exists(mut data_path: PathBuf) -> PathBuf {
103 data_path.push("qq-farm-bot");
104 if data_path.exists() && data_path.is_dir() {
105 info!("目录存在,无需下载");
106 } else {
107 info!("目录不存在,触发下载请求");
108 download(data_path.clone()).await.unwrap();
109 }
110
111 info!("正在执行 npm install...");
113
114 let status = if cfg!(target_os = "windows") {
115 Command::new("cmd")
116 .args(["/C", "npm", "install"])
117 .current_dir(&data_path)
118 .status()
119 } else {
120 Command::new("npm")
121 .arg("install")
122 .current_dir(&data_path)
123 .status()
124 };
125
126 match status.await {
127 Ok(s) if s.success() => info!("npm install 完成"),
128 Ok(s) => error!("npm install 失败,退出码: {:?}", s.code()),
129 Err(e) => error!("npm install 执行失败: {}", e),
130 }
131
132 data_path
133}
134
135async fn download(path: PathBuf) -> Result<String, Box<dyn std::error::Error>> {
136 let owner = "ryunnet";
137 let repo = "qq-farm-bot";
138 let branch = "main";
139
140 let url = format!(
141 "https://github.com/{owner}/{repo}/archive/refs/heads/{branch}.zip"
142 );
143
144 info!("正在下载 {owner}/{repo}...");
145 let response = reqwest::get(&url).await?;
146
147 if !response.status().is_success() {
148 return Err(format!("下载失败: {}", response.status()).into());
149 }
150
151 let bytes = response.bytes().await?;
152
153 let cursor = Cursor::new(bytes);
155 let mut archive = zip::ZipArchive::new(cursor)?;
156
157 let output_dir = path;
158
159 for i in 0..archive.len() {
160 let mut entry = archive.by_index(i)?;
161 let name = entry.name().to_string();
162
163 let stripped = match name.find('/') {
165 Some(pos) => &name[pos + 1..],
166 None => continue,
167 };
168
169 if stripped.is_empty() {
171 continue;
172 }
173
174 let out_path = output_dir.join(stripped);
175
176 if entry.is_dir() {
177 fs::create_dir_all(&out_path)?;
178 } else {
179 if let Some(parent) = out_path.parent() {
180 fs::create_dir_all(parent)?;
181 }
182 let mut buf = Vec::new();
183 entry.read_to_end(&mut buf)?;
184 let mut file = File::create(&out_path)?;
185 file.write_all(&buf)?;
186 }
187 }
188
189 info!("解压完成到 {:?}", output_dir);
190
191 return Ok(String::new());
192}
193
194async fn get_login_url(event: Arc<MsgEvent>, path: PathBuf) -> Data {
195 let client = Client::builder().build().unwrap();
196 let res = client.get("https://q.qq.com/ide/devtoolAuth/GetLoginCode").headers(get_headers()).send().await.unwrap();
197 let text = res.text().await.unwrap();
198 let res: Login = serde_json::from_str(&text).unwrap();
199 info!("{:#?}", res);
200
201 let data = Data {
202 code: res.data.code.clone(),
203 url: format!("https://h5.qzone.qq.com/qqq/code/{}?_proxy=1&from=ide", res.data.code.clone())
204 };
205
206 let code = data.code.clone();
207 tokio::spawn(async move {
208 let result = timeout(Duration::from_secs(300), async {
209 let mut ticker = interval(Duration::from_secs(2));
210 loop {
211 ticker.tick().await;
212 let data = check_login_status(code.clone()).await;
213 if let Some(ok) = data.data.ok {
214 if ok == 1 {
215 info!("data = {:?}", data);
216 event.reply(format!("正在为[ {} ]启动脚本!", event.get_sender_nickname()));
217 let uid = event.user_id.to_string();
218 if let Some(mut child) = STATUS.get_mut(&uid) {
219 child.child.kill().await.ok();
220 }
221 STATUS.remove(&uid);
222 let code = get_auth_code(data.data.ticket.unwrap()).await;
223 start(code, path.clone(), uid).await;
224 break;
225 }
226 }
227 }
228 }).await;
229 });
230
231 data
232}
233
234async fn start(code: String, path: PathBuf, user_id: String) {
235 let mut child = if cfg!(target_os = "windows") {
236 Command::new("cmd")
237 .args(["/C", "node", "client.js", "--code", code.as_str()])
238 .stdout(Stdio::piped())
239 .stderr(Stdio::piped())
240 .current_dir(&path)
241 .kill_on_drop(true)
242 .spawn().unwrap()
243 } else {
244 Command::new("node")
245 .args(["client.js", "--code", code.as_str()])
246 .stdout(Stdio::piped())
247 .stderr(Stdio::piped())
248 .current_dir(&path)
249 .kill_on_drop(true)
250 .spawn().unwrap()
251 };
252 let stdout = child.stdout.take().unwrap();
253 let stderr = child.stderr.take().unwrap();
254 let output = Arc::new(Mutex::new(VecDeque::new()));
255
256 let process = Process {
257 child,
258 output: Arc::clone(&output),
259 };
260 STATUS.insert(user_id.clone(), process);
261
262 let stdout_uid = user_id.clone();
264 let stdout_output = Arc::clone(&output);
265 tokio::spawn(async move {
266 let mut reader = BufReader::new(stdout).lines();
267 while let Ok(Some(line)) = reader.next_line().await {
268 println!("[{}] {}", stdout_uid, line);
269
270 let is_error = line.contains("连接未打开")
272 || line.contains("检查失败")
273 || line.contains("巡查失败")
274 || line.contains("连接失败");
275
276 let mut output = stdout_output.lock().await;
277 if output.len() >= 10 {
278 output.pop_front();
279 }
280
281 if is_error {
282 output.push_back(format!("[错误] {}", line));
283 drop(output);
284
285 if let Some(mut entry) = STATUS.get_mut(&stdout_uid) {
287 entry.child.kill().await.ok();
288 }
289 STATUS.remove(&stdout_uid);
290 error!("[{}] 检测到连接错误,已终止进程", stdout_uid);
291 break;
292 } else {
293 output.push_back(line);
294 }
295 }
296 });
297
298 let stderr_uid = user_id.clone();
300 tokio::spawn(async move {
301 let mut reader = BufReader::new(stderr).lines();
302 while let Ok(Some(line)) = reader.next_line().await {
303 error!("[{}] stderr: {}", stderr_uid, line);
304 let mut output = output.lock().await;
305 if output.len() >= 10 {
306 output.pop_front();
307 }
308 output.push_back(format!("[异常] {}", line));
309 drop(output);
310
311 if let Some(mut entry) = STATUS.get_mut(&stderr_uid) {
313 entry.child.kill().await.ok();
314 }
315 STATUS.remove(&stderr_uid);
316 error!("[{}] 检测到异常输出,已终止进程", stderr_uid);
317 break;
318 }
319 });
320}
321
322async fn check_login_status(code: String) -> Login {
323 let client = Client::builder().build().unwrap();
324 let url = format!("https://q.qq.com/ide/devtoolAuth/syncScanSateGetTicket?code={code}");
325 let res = client.get(url).headers(get_headers()).send().await.unwrap();
326 let text = res.text().await.unwrap();
327
328 let data: Login = serde_json::from_str(text.as_str()).unwrap();
329
330 data
331}
332
333#[derive(Deserialize, Debug)]
334struct Data {
335 code: String,
336 url: String
337}
338
339#[derive(Deserialize, Debug)]
340struct Login {
341 code: i16,
342 data: LoginData,
343 message: Option<String>,
344}
345
346#[derive(Deserialize, Debug)]
347struct LoginData {
348 code: String,
349 ticket: Option<String>,
350 ok: Option<i16>,
351 uin: Option<String>
352}
353
354fn get_headers() -> HeaderMap {
355 let mut headers = HeaderMap::new();
356 headers.insert("content-type", "application/json".parse().unwrap());
357 headers.insert("qua", "V1_HT5_QDT_0.70.2209190_x64_0_DEV_D".parse().unwrap());
358 headers.insert("host", "q.qq.com".parse().unwrap());
359 headers.insert("accept", "application/json".parse().unwrap());
360 headers
361}
362
363pub async fn get_auth_code(ticket: String) -> String{
364 let client = Client::builder().build().unwrap();
365 let url = "https://q.qq.com/ide/login".to_string();
366 let auth = Auth {
367 appid: String::from("1112386029"),
368 ticket: ticket.clone()
369 };
370 let res = client.post(url).headers(get_headers()).json(&auth).send().await.unwrap();
371 let text = res.text().await.unwrap();
372 println!("{:#?}", text);
373 let json = serde_json::from_str::<AuthCode>(text.as_str()).unwrap();
374 json.code
375}
376
377#[derive(Deserialize, Debug)]
378struct AuthCode {
379 code: String,
380 message: String,
381}
382
383#[derive(Deserialize, Serialize)]
384struct Auth {
385 appid: String,
386 ticket: String,
387}