mirror of
http://shenjack.top:5100/shenjack/icalingua-python-bot.git
synced 2025-04-20 08:59:54 +08:00
Compare commits
5 Commits
9ce466f000
...
df0355cad7
Author | SHA1 | Date | |
---|---|---|---|
df0355cad7 | |||
75e7e0085a | |||
bbaf8cf0c9 | |||
e5fde06837 | |||
974368166b |
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "ica-rs"
|
||||
version = "0.4.2"
|
||||
version = "0.4.4"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
@ -20,12 +20,13 @@ colored = "2.1.0"
|
|||
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
futures-util = "0.3.30"
|
||||
pyo3 = "0.20.2"
|
||||
# pyo3-async = "0.3.2"
|
||||
pyo3-asyncio = { version = "0.20.0", features = ["attributes", "tokio-runtime"] }
|
||||
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["time"] }
|
||||
|
||||
[dependencies.pyo3]
|
||||
version = "0.20.2"
|
||||
|
||||
[patch.crates-io]
|
||||
rust_socketio = { git = "https://github.com/shenjackyuanjie/rust-socketio.git", branch = "mult_payload" }
|
||||
# pyo3 = { git = "https://github.com/PyO3/pyo3.git", branch = "main" }
|
||||
|
|
|
@ -44,3 +44,36 @@ class SendMessage:
|
|||
class NewMessage:
|
||||
def reply_with(self, message: str) -> SendMessage:
|
||||
...
|
||||
def __str__(self) -> str:
|
||||
...
|
||||
@property
|
||||
def content(self) -> str:
|
||||
...
|
||||
@property
|
||||
def sender_id(self) -> int:
|
||||
...
|
||||
@property
|
||||
def is_from_self(self) -> bool:
|
||||
...
|
||||
|
||||
|
||||
class IcaClient:
|
||||
@staticmethod
|
||||
async def send_message_a(client: "IcaClient", message: SendMessage) -> bool:
|
||||
"""
|
||||
仅作占位
|
||||
(因为目前来说, rust调用 Python端没法启动一个异步运行时
|
||||
所以只能 tokio::task::block_in_place 转换成同步调用)
|
||||
"""
|
||||
def send_message(self, message: SendMessage) -> bool:
|
||||
...
|
||||
def debug(self, message: str) -> None:
|
||||
...
|
||||
def info(self, message: str) -> None:
|
||||
...
|
||||
def warn(self, message: str) -> None:
|
||||
...
|
||||
|
||||
|
||||
def on_message(msg: NewMessage, client: IcaClient) -> None:
|
||||
...
|
||||
|
|
15
ica-rs/plugins/base.py
Normal file
15
ica-rs/plugins/base.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ica_typing import NewMessage, IcaClient
|
||||
else:
|
||||
NewMessage = TypeVar("NewMessage")
|
||||
IcaClient = TypeVar("IcaClient")
|
||||
|
||||
_version_ = "1.0.0"
|
||||
|
||||
def on_message(msg: NewMessage, client: IcaClient) -> None:
|
||||
if not msg.is_from_self:
|
||||
if msg.content == "/bot-rs-py":
|
||||
reply = msg.reply_with(f"ica-async-rs-sync-py {_version_}")
|
||||
client.send_message(reply)
|
86
ica-rs/plugins/bmcl.py
Normal file
86
ica-rs/plugins/bmcl.py
Normal file
|
@ -0,0 +1,86 @@
|
|||
|
||||
import time
|
||||
import requests
|
||||
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ica_typing import NewMessage, IcaClient
|
||||
else:
|
||||
NewMessage = TypeVar("NewMessage")
|
||||
IcaClient = TypeVar("IcaClient")
|
||||
|
||||
_version_ = "2.0.0-rs"
|
||||
|
||||
def format_data_size(data_bytes: float) -> str:
|
||||
data_lens = ["B", "KB", "MB", "GB", "TB"]
|
||||
data_len = "0B"
|
||||
for i in range(5):
|
||||
if data_bytes < 1024:
|
||||
data_bytes = round(data_bytes, 5)
|
||||
data_len = f"{data_bytes}{data_lens[i]}"
|
||||
break
|
||||
else:
|
||||
data_bytes /= 1024
|
||||
return data_len
|
||||
|
||||
|
||||
def format_hit_count(count: int) -> str:
|
||||
"""数据分段, 四位一个下划线
|
||||
|
||||
Args:
|
||||
count (int): 数据
|
||||
|
||||
Returns:
|
||||
str: 格式化后的数据
|
||||
1 -> 1
|
||||
1000 -> 1000
|
||||
10000 -> 1_0000
|
||||
100000 -> 10_0000
|
||||
1000000 -> 100_0000
|
||||
"""
|
||||
count_str = str(count)
|
||||
count_len = len(count_str)
|
||||
if count_len <= 4:
|
||||
return count_str
|
||||
else:
|
||||
return "_".join(count_str[i:i + 4] for i in range(0, count_len, 4))
|
||||
|
||||
def bmcl(msg: NewMessage, client: IcaClient) -> None:
|
||||
req_time = time.time()
|
||||
# 记录请求时间
|
||||
response = requests.get("https://bd.bangbang93.com/openbmclapi/metric/dashboard")
|
||||
if not response.status_code == 200 or response.reason != "OK":
|
||||
reply = msg.reply_with(f"请求数据失败\n{response.status_code}")
|
||||
client.warn(
|
||||
f"数据请求失败, 请检查网络\n{response.status}"
|
||||
)
|
||||
client.send_message(reply)
|
||||
return
|
||||
data = response.json()
|
||||
data_bytes: float = data["bytes"]
|
||||
data_hits: int = data["hits"]
|
||||
data_bandwidth: float = data["currentBandwidth"]
|
||||
load_str: float = data["load"] * 100
|
||||
online_node: int = data["currentNodes"]
|
||||
online_bandwidth: int = data["bandwidth"]
|
||||
data_len = format_data_size(data_bytes)
|
||||
hits_count = format_hit_count(data_hits)
|
||||
|
||||
report_msg = (
|
||||
f"OpenBMCLAPI 状态面板v{_version_} :\n"
|
||||
f"实时信息: {online_node} 带宽: {online_bandwidth}Mbps\n"
|
||||
f"负载: {load_str:.2f}% 带宽: {data_bandwidth:.2f}Mbps\n"
|
||||
f"当日请求: {hits_count} 数据量: {data_len}\n"
|
||||
f"请求时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(req_time))}\n"
|
||||
"数据源: https://bd.bangbang93.com/pages/dashboard"
|
||||
)
|
||||
client.info(report_msg)
|
||||
reply = msg.reply_with(report_msg)
|
||||
client.send_message(reply)
|
||||
|
||||
|
||||
def on_message(msg: NewMessage, client: IcaClient) -> None:
|
||||
if not msg.is_from_self:
|
||||
if msg.content == "/bmcl-rs":
|
||||
bmcl(msg, client)
|
|
@ -11,11 +11,17 @@ use serde_json::Value;
|
|||
use tracing::{debug, warn};
|
||||
|
||||
/// "安全" 的 发送一条消息
|
||||
pub async fn send_message(client: Client, message: SendMessage) {
|
||||
pub async fn send_message(client: &Client, message: &SendMessage) -> bool {
|
||||
let value = message.as_value();
|
||||
match client.emit("sendMessage", value).await {
|
||||
Ok(_) => debug!("send_message {}", format!("{:#?}", message).cyan()),
|
||||
Err(e) => warn!("send_message faild:{}", format!("{:#?}", e).red()),
|
||||
Ok(_) => {
|
||||
debug!("send_message {}", format!("{:#?}", message).cyan());
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("send_message faild:{}", format!("{:#?}", e).red());
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,18 +31,20 @@ pub async fn add_message(payload: Payload, client: Client) {
|
|||
if let Some(value) = values.first() {
|
||||
let message = NewMessage::new_from_json(value);
|
||||
info!("add_message {}", format!("{:#?}", message).cyan());
|
||||
if message.is_reply() {
|
||||
return;
|
||||
}
|
||||
if message.is_from_self() {
|
||||
return;
|
||||
}
|
||||
// if message.is_reply() {
|
||||
// return;
|
||||
// }
|
||||
// if message.is_from_self() {
|
||||
// return;
|
||||
// }
|
||||
// 就在这里处理掉最基本的消息
|
||||
// 之后的处理交给插件
|
||||
if message.content.eq("/bot-rs") {
|
||||
if message.content.eq("/bot-rs") && !message.is_from_self() && !message.is_reply() {
|
||||
let reply = message.reply_with(&format!("ica-async-rs pong v{}", VERSION));
|
||||
send_message(client, reply).await;
|
||||
send_message(&client, &reply).await;
|
||||
}
|
||||
// python 插件
|
||||
py::new_message_py(&message, &client).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -135,7 +137,6 @@ pub async fn connect_callback(payload: Payload, _client: Client) {
|
|||
if let Some(value) = values.first() {
|
||||
match value.as_str() {
|
||||
Some("authSucceed") => {
|
||||
py::run();
|
||||
info!("{}", "已经登录到 icalingua!".green())
|
||||
}
|
||||
Some("authFailed") => {
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
use pyo3::prelude::*;
|
||||
use tracing::{debug, info, warn};
|
||||
use rust_socketio::asynchronous::Client;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use crate::client::send_message;
|
||||
use crate::data_struct::messages::{NewMessage, ReplyMessage, SendMessage};
|
||||
use crate::ClientStatus;
|
||||
|
||||
|
@ -106,6 +110,7 @@ impl IcaStatusPy {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[pyclass]
|
||||
#[pyo3(name = "NewMessage")]
|
||||
pub struct NewMessagePy {
|
||||
|
@ -117,6 +122,23 @@ impl NewMessagePy {
|
|||
pub fn reply_with(&self, content: String) -> SendMessagePy {
|
||||
SendMessagePy::new(self.msg.reply_with(&content))
|
||||
}
|
||||
|
||||
pub fn __str__(&self) -> String {
|
||||
format!("{:?}", self.msg)
|
||||
}
|
||||
|
||||
#[getter]
|
||||
pub fn get_content(&self) -> String {
|
||||
self.msg.content.clone()
|
||||
}
|
||||
#[getter]
|
||||
pub fn get_sender_id(&self) -> i64 {
|
||||
self.msg.sender_id
|
||||
}
|
||||
#[getter]
|
||||
pub fn get_is_from_self(&self) -> bool {
|
||||
self.msg.is_from_self()
|
||||
}
|
||||
}
|
||||
|
||||
impl NewMessagePy {
|
||||
|
@ -131,20 +153,86 @@ pub struct ReplyMessagePy {
|
|||
pub msg: ReplyMessage,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl ReplyMessagePy {
|
||||
pub fn __str__(&self) -> String {
|
||||
format!("{:?}", self.msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl ReplyMessagePy {
|
||||
pub fn new(msg: ReplyMessage) -> Self {
|
||||
Self { msg }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[pyclass]
|
||||
#[pyo3(name = "SendMessage")]
|
||||
pub struct SendMessagePy {
|
||||
pub msg: SendMessage,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl SendMessagePy {
|
||||
pub fn __str__(&self) -> String {
|
||||
format!("{:?}", self.msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl SendMessagePy {
|
||||
pub fn new(msg: SendMessage) -> Self {
|
||||
Self { msg }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[pyclass]
|
||||
#[pyo3(name = "IcaClient")]
|
||||
pub struct IcaClientPy {
|
||||
pub client: Client,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl IcaClientPy {
|
||||
pub fn send_message(&self, message: SendMessagePy) -> bool {
|
||||
tokio::task::block_in_place(|| {
|
||||
let rt = Runtime::new().unwrap();
|
||||
rt.block_on(send_message(&self.client, &message.msg))
|
||||
})
|
||||
}
|
||||
|
||||
/// 仅作占位
|
||||
/// (因为目前来说, rust调用 Python端没法启动一个异步运行时
|
||||
/// 所以只能 tokio::task::block_in_place 转换成同步调用)
|
||||
#[staticmethod]
|
||||
pub fn send_message_a(
|
||||
py: Python,
|
||||
client: IcaClientPy,
|
||||
message: SendMessagePy,
|
||||
) -> PyResult<&PyAny> {
|
||||
pyo3_asyncio::tokio::future_into_py(py, async move {
|
||||
Ok(send_message(&client.client, &message.msg).await)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn debug(&self, content: String) {
|
||||
debug!("{}", content);
|
||||
}
|
||||
|
||||
pub fn info(&self, content: String) {
|
||||
info!("{}", content);
|
||||
}
|
||||
|
||||
pub fn warn(&self, content: String) {
|
||||
warn!("{}", content);
|
||||
}
|
||||
}
|
||||
|
||||
impl IcaClientPy {
|
||||
pub fn new(client: &Client) -> Self {
|
||||
Self {
|
||||
client: client.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
pub mod class;
|
||||
|
||||
use std::time::SystemTime;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use blake3::Hasher;
|
||||
use pyo3::{prelude::*, types::IntoPyDict};
|
||||
use rust_socketio::asynchronous::Client;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::config::IcaConfig;
|
||||
use crate::data_struct::messages::NewMessage;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PyStatus {
|
||||
pub files: Option<HashMap<PathBuf, (Vec<u8>, String)>>,
|
||||
pub files: Option<HashMap<PathBuf, (Option<SystemTime>, Py<PyAny>)>>,
|
||||
}
|
||||
|
||||
impl PyStatus {
|
||||
pub fn get_files() -> &'static HashMap<PathBuf, (Vec<u8>, String)> {
|
||||
pub fn get_files() -> &'static HashMap<PathBuf, (Option<SystemTime>, Py<PyAny>)> {
|
||||
unsafe {
|
||||
match PYSTATUS.files.as_ref() {
|
||||
Some(files) => files,
|
||||
|
@ -27,26 +29,37 @@ impl PyStatus {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn add_file(path: PathBuf, content: Vec<u8>, hash: String) {
|
||||
pub fn add_file(path: PathBuf, changed_time: Option<SystemTime>, py_module: Py<PyAny>) {
|
||||
unsafe {
|
||||
match PYSTATUS.files.as_mut() {
|
||||
Some(files) => {
|
||||
files.insert(path, (content, hash));
|
||||
files.insert(path, (changed_time, py_module));
|
||||
debug!("Added file to py status, {:?}", files);
|
||||
}
|
||||
None => {
|
||||
warn!("No files in py status, creating new");
|
||||
let mut files = HashMap::new();
|
||||
files.insert(path, (content, hash));
|
||||
files.insert(path, (changed_time, py_module));
|
||||
PYSTATUS.files = Some(files);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_file(path: &PathBuf, hash: &String) -> bool {
|
||||
pub fn verify_file(path: &PathBuf) -> bool {
|
||||
unsafe {
|
||||
match PYSTATUS.files.as_ref() {
|
||||
Some(files) => match files.get(path) {
|
||||
Some((_, file_hash)) => file_hash == hash,
|
||||
Some((changed_time, _)) => {
|
||||
if let Some(changed_time) = changed_time {
|
||||
if let Some(new_changed_time) = get_change_time(path) {
|
||||
if new_changed_time != *changed_time {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
},
|
||||
None => false,
|
||||
},
|
||||
None => false,
|
||||
|
@ -57,27 +70,97 @@ impl PyStatus {
|
|||
|
||||
pub static mut PYSTATUS: PyStatus = PyStatus { files: None };
|
||||
|
||||
pub fn run() {
|
||||
Python::with_gil(|py| {
|
||||
let bot_status = class::IcaStatusPy::new();
|
||||
let _bot_status: &PyCell<_> = PyCell::new(py, bot_status).unwrap();
|
||||
|
||||
let locals = [("state", _bot_status)].into_py_dict(py);
|
||||
py.run(
|
||||
"from pathlib import Path\nprint(Path.cwd())\nprint(state)",
|
||||
None,
|
||||
Some(locals),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
pub fn load_py_plugins(path: &PathBuf) {
|
||||
if path.exists() {
|
||||
info!("finding plugins in: {:?}", path);
|
||||
// 搜索所有的 py 文件 和 文件夹单层下面的 py 文件
|
||||
match path.read_dir() {
|
||||
Err(e) => {
|
||||
warn!("failed to read plugin path: {:?}", e);
|
||||
}
|
||||
Ok(dir) => {
|
||||
for entry in dir {
|
||||
if let Ok(entry) = entry {
|
||||
let path = entry.path();
|
||||
if let Some(ext) = path.extension() {
|
||||
if ext == "py" {
|
||||
match load_py_file(&path) {
|
||||
Ok((changed_time, content)) => {
|
||||
let py_module: PyResult<Py<PyAny>> = Python::with_gil(|py| -> PyResult<Py<PyAny>> {
|
||||
let module: PyResult<Py<PyAny>> = PyModule::from_code(
|
||||
py,
|
||||
&content,
|
||||
&path.to_string_lossy(),
|
||||
&path.to_string_lossy()
|
||||
)
|
||||
.map(|module| module.into());
|
||||
module
|
||||
});
|
||||
match py_module {
|
||||
Ok(py_module) => {
|
||||
info!("加载到插件: {:?}", path);
|
||||
PyStatus::add_file(path, changed_time, py_module);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("failed to load file: {:?} | e: {:?}", path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("failed to load file: {:?} | e: {:?}", path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("plugin path not exists: {:?}", path);
|
||||
}
|
||||
info!(
|
||||
"python 插件目录: {:?} 加载完成, 加载到 {} 个插件",
|
||||
path,
|
||||
PyStatus::get_files().len()
|
||||
);
|
||||
}
|
||||
|
||||
pub fn load_py_file(path: &PathBuf) -> (Vec<u8>, String) {
|
||||
let mut hasher = Hasher::new();
|
||||
let content = std::fs::read(path).unwrap();
|
||||
hasher.update(&content);
|
||||
let hash = hasher.finalize().as_bytes().to_vec();
|
||||
(content, hex::encode(hash))
|
||||
pub fn verify_plugins() {
|
||||
let plugins = PyStatus::get_files();
|
||||
for (path, _) in plugins.iter() {
|
||||
if !PyStatus::verify_file(path) {
|
||||
info!("file changed: {:?}", path);
|
||||
if let Ok((changed_time, content)) = load_py_file(path) {
|
||||
let py_module = Python::with_gil(|py| -> Py<PyAny> {
|
||||
let module: Py<PyAny> = PyModule::from_code(
|
||||
py,
|
||||
&content,
|
||||
&path.to_string_lossy(),
|
||||
&path.to_string_lossy(),
|
||||
// !!!! 请注意, 一定要给他一个名字, cpython 会自动把后面的重名模块覆盖掉前面的
|
||||
)
|
||||
.unwrap()
|
||||
.into();
|
||||
module
|
||||
});
|
||||
PyStatus::add_file(path.clone(), changed_time, py_module);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub fn get_change_time(path: &PathBuf) -> Option<SystemTime> {
|
||||
path.metadata().ok()?.modified().ok()
|
||||
}
|
||||
|
||||
/// 传入文件路径
|
||||
/// 返回 hash 和 文件内容
|
||||
pub fn load_py_file(path: &PathBuf) -> std::io::Result<(Option<SystemTime>, String)> {
|
||||
let changed_time = get_change_time(&path);
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
Ok((changed_time, content))
|
||||
}
|
||||
|
||||
pub fn init_py(config: &IcaConfig) {
|
||||
|
@ -85,28 +168,47 @@ pub fn init_py(config: &IcaConfig) {
|
|||
pyo3::prepare_freethreaded_python();
|
||||
if let Some(plugin_path) = &config.py_plugin_path {
|
||||
let path = PathBuf::from(plugin_path);
|
||||
if path.exists() {
|
||||
info!("finding plugins in: {:?}", path);
|
||||
// 搜索所有的 py 文件 和 文件夹单层下面的 py 文件
|
||||
match path.read_dir() {
|
||||
Err(e) => {
|
||||
warn!("failed to read plugin path: {:?}", e);
|
||||
}
|
||||
Ok(dir) => {
|
||||
for entry in dir {
|
||||
if let Ok(entry) = entry {
|
||||
let path = entry.path();
|
||||
if let Some(ext) = path.extension() {
|
||||
if ext == "py" {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("plugin path not exists: {:?}", path);
|
||||
}
|
||||
load_py_plugins(&path);
|
||||
debug!("python 插件列表: {:#?}", PyStatus::get_files());
|
||||
}
|
||||
|
||||
info!("python inited")
|
||||
}
|
||||
|
||||
/// 执行 new message 的 python 插件
|
||||
pub async fn new_message_py(message: &NewMessage, client: &Client) {
|
||||
// 验证插件是否改变
|
||||
verify_plugins();
|
||||
let cwd = std::env::current_dir().unwrap();
|
||||
|
||||
let plugins = PyStatus::get_files();
|
||||
for (path, (_, py_module)) in plugins.iter() {
|
||||
// 切换工作目录到运行的插件的位置
|
||||
let mut goto = cwd.clone();
|
||||
goto.push(path.parent().unwrap());
|
||||
|
||||
if let Err(e) = std::env::set_current_dir(&goto) {
|
||||
warn!("移动工作目录到 {:?} 失败 {:?} cwd: {:?}", goto, e, cwd);
|
||||
}
|
||||
|
||||
Python::with_gil(|py| {
|
||||
let msg = class::NewMessagePy::new(message);
|
||||
let client = class::IcaClientPy::new(client);
|
||||
let args = (msg, client);
|
||||
let async_py_func = py_module.getattr(py, "on_message");
|
||||
match async_py_func {
|
||||
Ok(async_py_func) => {
|
||||
async_py_func.as_ref(py).call1(args).unwrap();
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("failed to get on_message function: {:?}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 最后切换回来
|
||||
if let Err(e) = std::env::set_current_dir(&cwd) {
|
||||
warn!("设置工作目录{:?} 失败:{:?}", cwd, e);
|
||||
}
|
||||
}
|
||||
|
|
9
news.md
9
news.md
|
@ -1,5 +1,14 @@
|
|||
# 更新日志
|
||||
|
||||
## 0.4.4
|
||||
|
||||
现在正式支持 Python 插件了
|
||||
`/bmcl` 也迁移到了 Python 插件版本
|
||||
|
||||
## 0.4.3
|
||||
|
||||
噫! 好! 我成了!
|
||||
|
||||
## 0.4.2
|
||||
|
||||
现在是 async 版本啦!
|
||||
|
|
20
readme.md
20
readme.md
|
@ -5,7 +5,7 @@
|
|||
> 出于某个企鹅, 和保护 作者 和 原项目 ( ica ) 的原因 \
|
||||
> 功能请自行理解
|
||||
|
||||
## 使用方法
|
||||
## 使用方法 ( Python 版 )
|
||||
|
||||
- 安装依赖
|
||||
|
||||
|
@ -40,3 +40,21 @@ docker up -d
|
|||
```powershell
|
||||
python connect.py
|
||||
```
|
||||
|
||||
## 使用方法 ( Rust 版 )
|
||||
|
||||
- 准备一个 Python 环境
|
||||
|
||||
- 修改好配置文件
|
||||
|
||||
```powershell
|
||||
Copy-Item config-temp.toml config.toml
|
||||
```
|
||||
|
||||
- 编译
|
||||
|
||||
```powershell
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
运行
|
||||
|
|
Loading…
Reference in New Issue
Block a user