feat: 添加守护进程(daemon)模式

- 实现 TCP Socket 守护进程,支持后台运行
- 添加 daemon 子命令:start/stop/status/logs
- 添加 send 子命令:发送文本到守护进程播放
- 添加日志级别自动检测(INFO/WARN/ERROR)
- 新日志格式:[时间戳] [级别] [PID] 消息
- 支持跨平台 ~/.config/tts/ 配置目录
This commit is contained in:
2026-04-25 05:50:28 +08:00
parent f81d1ab979
commit 19eb313bef
10 changed files with 1202 additions and 5 deletions

478
src/daemon.rs Normal file
View File

@@ -0,0 +1,478 @@
/// 守护进程模块
///
/// 实现 TCP Socket 服务器,接收客户端请求并执行 TTS 语音合成和播放
/// 类似 Docker daemon 模式,作为后台服务运行
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpListener;
use tokio::sync::mpsc;
/// 客户端请求结构
#[derive(Debug, Deserialize)]
pub struct DaemonRequest {
/// 要合成的文本
pub text: String,
/// 音色名称(可选,默认 mimo_default
#[serde(skip_serializing_if = "Option::is_none")]
pub voice: Option<String>,
/// 音频格式(可选,默认 wav
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
/// 风格描述(可选,宏观场景风格)
#[serde(skip_serializing_if = "Option::is_none")]
pub style: Option<String>,
}
/// 服务端响应结构
#[derive(Debug, Serialize)]
pub struct DaemonResponse {
/// 状态ok 或 error
pub status: String,
/// 消息
pub message: String,
}
/// 获取配置目录(所有平台统一使用 ~/.config/tts/
fn get_config_dir() -> Result<PathBuf> {
let home = dirs::home_dir().context("无法获取用户家目录")?;
let config_dir = home.join(".config").join("tts");
// 确保目录存在
if !config_dir.exists() {
fs::create_dir_all(&config_dir)
.with_context(|| format!("无法创建配置目录: {:?}", config_dir))?;
}
Ok(config_dir)
}
/// 获取 PID 文件路径
fn get_pid_file_path() -> Result<PathBuf> {
Ok(get_config_dir()?.join("ttsd.pid"))
}
/// 获取日志文件路径
fn get_log_file_path() -> Result<PathBuf> {
Ok(get_config_dir()?.join("ttsd.log"))
}
/// 获取 socket 文件路径(预留,未来可选 Unix Socket
#[allow(dead_code)]
fn get_socket_path() -> Result<PathBuf> {
Ok(get_config_dir()?.join("ttsd.sock"))
}
/// 日志级别
#[derive(Debug, PartialEq)]
enum LogLevel {
Info,
Warn,
Error,
}
impl LogLevel {
fn from_message(msg: &str) -> Self {
let lower = msg.to_lowercase();
if lower.contains("error") || lower.contains("失败") || lower.contains("无法") {
LogLevel::Error
} else if lower.contains("warning") || lower.contains("警告") || lower.contains("注意") {
LogLevel::Warn
} else {
LogLevel::Info
}
}
fn as_str(&self) -> &str {
match self {
LogLevel::Info => "INFO",
LogLevel::Warn => "WARN",
LogLevel::Error => "ERROR",
}
}
}
/// 写入日志
fn write_log(message: &str) -> Result<()> {
let log_path = get_log_file_path()?;
let now = std::time::SystemTime::now();
let datetime: chrono::DateTime<chrono::Local> = now.into();
let timestamp = datetime.format("%Y-%m-%d %H:%M:%S");
// 自动检测日志级别
let level = LogLevel::from_message(message);
// 获取当前 PID
let pid = std::process::id();
// 新格式:[时间戳] [级别] [PID] 消息
let log_line = format!("[{}] [{}] [{}] {}\n", timestamp, level.as_str(), pid, message);
let mut file = fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
.with_context(|| format!("无法打开日志文件: {:?}", log_path))?;
file.write_all(log_line.as_bytes())
.with_context(|| format!("无法写入日志文件: {:?}", log_path))?;
// 简化的 stdout 输出
if level == LogLevel::Error {
eprintln!("[Daemon {}] {}", pid, message);
} else {
println!("[Daemon {}] {}", pid, message);
}
Ok(())
}
/// 检查守护进程是否正在运行
pub fn is_running() -> bool {
let pid_path = match get_pid_file_path() {
Ok(p) => p,
Err(_) => return false,
};
if !pid_path.exists() {
return false;
}
let pid_content = match fs::read_to_string(&pid_path) {
Ok(s) => s,
Err(_) => return false,
};
let pid: u32 = match pid_content.trim().parse() {
Ok(p) => p,
Err(_) => {
// PID 文件内容无效,删除它
let _ = fs::remove_file(&pid_path);
return false;
}
};
// 检查进程是否存在(跨平台)
#[cfg(unix)]
{
use std::process::Command;
let output = Command::new("kill")
.args(["-0", &pid.to_string()])
.output();
match output {
Ok(o) => o.status.success(),
Err(_) => false,
}
}
#[cfg(windows)]
{
use std::process::Command;
let output = Command::new("tasklist")
.args(["/FI", &format!("PID eq {}", pid), "/NH"])
.output();
match output {
Ok(o) => {
let stdout = String::from_utf8_lossy(&o.stdout);
stdout.contains(&pid.to_string())
}
Err(_) => false,
}
}
}
/// 启动守护进程(纯循环,由调用者决定是否后台运行)
pub async fn start_daemon(port: u16) -> Result<()> {
// 检查是否已经在运行
if is_running() {
return Err(anyhow::anyhow!("守护进程已经在运行中"));
}
let addr = format!("127.0.0.1:{}", port);
write_log(&format!("正在启动守护进程,监听地址: {}", addr))?;
// 创建 TCP 监听器
let listener = TcpListener::bind(&addr)
.await
.with_context(|| format!("无法绑定地址: {}", addr))?;
write_log(&format!("守护进程已启动,监听: {}", addr))?;
// 写入 PID 文件
let pid = std::process::id();
let pid_path = get_pid_file_path()?;
fs::write(&pid_path, pid.to_string())
.with_context(|| format!("无法写入 PID 文件: {:?}", pid_path))?;
write_log(&format!("PID: {}, PID 文件: {:?}", pid, pid_path))?;
// 创建通道用于停止信号
let (tx, mut rx) = mpsc::channel::<()>(1);
// 处理 Ctrl+C
tokio::spawn(async move {
if let Ok(()) = tokio::signal::ctrl_c().await {
write_log("收到停止信号,正在关闭...").ok();
let _ = tx.send(()).await;
}
});
// 主循环:接受连接
loop {
tokio::select! {
// 接受新连接
accept_result = listener.accept() => {
match accept_result {
Ok((stream, addr)) => {
write_log(&format!("新连接来自: {}", addr))?;
// 为每个连接创建新任务
tokio::spawn(async move {
if let Err(e) = handle_client(stream).await {
let _ = write_log(&format!("处理连接失败: {:#}", e));
}
});
}
Err(e) => {
write_log(&format!("接受连接失败: {}", e)).ok();
}
}
}
// 收到停止信号
_ = rx.recv() => {
write_log("正在停止守护进程...").ok();
break;
}
}
}
// 清理 PID 文件
let _ = fs::remove_file(&pid_path);
write_log("守护进程已停止").ok();
Ok(())
}
/// 处理单个客户端连接
async fn handle_client(stream: tokio::net::TcpStream) -> Result<()> {
let (reader, mut writer) = stream.into_split();
let mut reader = BufReader::new(reader);
let mut line = String::new();
// 读取一行 JSON 请求
let bytes_read = reader.read_line(&mut line).await?;
if bytes_read == 0 {
return Err(anyhow::anyhow!("连接已关闭"));
}
write_log(&format!("收到请求: {}", line.trim()))?;
// 解析请求
let response = match serde_json::from_str::<DaemonRequest>(&line) {
Ok(request) => {
// 处理 TTS 请求
match process_tts_request(request).await {
Ok(msg) => DaemonResponse {
status: "ok".to_string(),
message: msg,
},
Err(e) => {
write_log(&format!("TTS 处理失败: {:#}", e)).ok();
DaemonResponse {
status: "error".to_string(),
message: format!("{:#}", e),
}
}
}
}
Err(e) => {
write_log(&format!("解析请求失败: {}", e)).ok();
DaemonResponse {
status: "error".to_string(),
message: format!("无效的请求格式: {}", e),
}
}
};
// 发送响应
let response_json = serde_json::to_string(&response)?;
writer.write_all(response_json.as_bytes()).await?;
writer.write_all(b"\n").await?;
writer.flush().await?;
write_log(&format!("响应: {}", response_json))?;
Ok(())
}
/// 处理 TTS 请求
async fn process_tts_request(request: DaemonRequest) -> Result<String> {
let text = request.text;
let voice = request.voice.unwrap_or_else(|| "mimo_default".to_string());
let format = request.format.unwrap_or_else(|| "wav".to_string());
let style = request.style;
write_log(&format!("处理 TTS: text={}, voice={}, format={}, style={:?}",
&text[..text.len().min(50)], voice, format, style))?;
// 处理风格标签
let mut final_text = text.clone();
if let Some(style_value) = &style {
// 按官方文档,使用 <style>...</style> 标签
final_text = format!("<style>{}</style>{}", style_value, final_text);
write_log(&format!("添加风格标签: {}", style_value))?;
}
// 加载配置
let config_manager = crate::config::ConfigManager::new()
.context("无法加载配置")?;
let config = config_manager.get_config();
// 检查 API Key
if config.api_key.is_empty() {
return Err(anyhow::anyhow!(
"API Key 未设置,请使用: mimo-tts config set --api-key <YOUR_API_KEY>"
));
}
// 创建 TTS 客户端
let client = crate::api::TtsClient::builder()
.base_url(config.base_url.clone())
.api_key(config.api_key.clone())
.build()
.context("无法创建 TTS 客户端")?;
// 构建请求
let mut builder = crate::api::TtsRequest::builder()
.audio(crate::api::AudioConfig {
format: format.clone(),
voice: voice.clone(),
});
// 添加 assistant 消息(实际要合成的文本)
builder = builder.add_message(crate::api::Message {
role: "assistant".to_string(),
content: final_text.clone(),
});
let tts_request = builder.build();
// 调用 API 合成语音
write_log("正在调用 TTS API...")?;
let audio_data = client
.synthesize_with_request(&tts_request)
.await
.context("语音合成失败")?;
write_log(&format!("TTS 成功,音频数据大小: {} 字节", audio_data.len()))?;
// 播放音频
write_log("正在播放音频...")?;
crate::play_audio(&audio_data)?;
Ok("播放完成".to_string())
}
/// 停止守护进程
pub fn stop_daemon() -> Result<()> {
if !is_running() {
return Err(anyhow::anyhow!("守护进程未运行"));
}
let pid_path = get_pid_file_path()?;
let pid_content = fs::read_to_string(&pid_path)
.with_context(|| format!("无法读取 PID 文件: {:?}", pid_path))?;
let pid: u32 = pid_content.trim().parse()
.with_context(|| format!("无效的 PID: {}", pid_content))?;
write_log(&format!("正在停止守护进程PID: {}", pid))?;
// 发送终止信号(跨平台)
#[cfg(unix)]
{
use std::process::Command;
Command::new("kill")
.arg(pid.to_string())
.output()
.with_context(|| format!("无法停止进程: {}", pid))?;
}
#[cfg(windows)]
{
use std::process::Command;
Command::new("taskkill")
.args(["/PID", &pid.to_string(), "/F"])
.output()
.with_context(|| format!("无法停止进程: {}", pid))?;
}
// 等待进程停止
std::thread::sleep(std::time::Duration::from_secs(1));
// 清理 PID 文件
if pid_path.exists() {
fs::remove_file(&pid_path)
.with_context(|| format!("无法删除 PID 文件: {:?}", pid_path))?;
}
write_log("守护进程已停止")?;
Ok(())
}
/// 显示守护进程状态
pub fn show_status() -> Result<()> {
if is_running() {
let pid_path = get_pid_file_path()?;
let pid_content = fs::read_to_string(&pid_path)
.with_context(|| format!("无法读取 PID 文件: {:?}", pid_path))?;
println!("守护进程状态: 运行中");
println!("PID: {}", pid_content.trim());
println!("日志文件: {:?}", get_log_file_path()?);
} else {
println!("守护进程状态: 未运行");
}
Ok(())
}
/// 显示守护进程日志
pub fn show_logs(lines: u32) -> Result<()> {
let log_path = get_log_file_path()?;
if !log_path.exists() {
println!("日志文件不存在: {:?}", log_path);
return Ok(());
}
let content = fs::read_to_string(&log_path)
.with_context(|| format!("无法读取日志文件: {:?}", log_path))?;
let all_lines: Vec<&str> = content.lines().collect();
let total_lines = all_lines.len();
let start = if total_lines > lines as usize {
total_lines - lines as usize
} else {
0
};
println!("===== 守护进程日志 (最近 {} 行) =====", lines);
println!("日志文件: {:?}\n", log_path);
for line in &all_lines[start..] {
println!("{}", line);
}
Ok(())
}