docs: 战略规划和管理架构优化

- 新增 STRATEGY.md: 三年战略规划、技术路线、团队策略
- 新增 MILESTONES.md: 详细里程碑和时间表(M1.1-M1.4)
- 新增 CODE_REVIEW.md: 代码审核标准和流程
- 组建管理班子: 新增 PM 刘建国,优化管理架构
- 丰富团队成员背景: 补充所有成员的教育经历、工作经验、技能树
- 解锁多线程思考能力: 团队成员可使用 kilo 命令并行探索
- 更新工作流程: CEO → PM → 开发团队,两级审核制度
- 修正 kilo 调用方式: 不使用 -f 参数,在消息中指示读取文件
This commit is contained in:
showen
2026-03-12 06:14:52 +08:00
parent 98ba7704dd
commit d443f28f6e
22 changed files with 3572 additions and 100 deletions

View File

@@ -2,17 +2,112 @@
//!
//! 基于 warp 的 HTTP 服务,提供播放控制、配置管理、视频管理等 API。
use crate::core::message::Message;
mod routes;
use crate::core::config::AppConfig;
use crate::core::message::{Envelope, Message};
use crate::core::plugin::{Plugin, PluginContext, PluginInfo, Platform};
use anyhow::Result;
use anyhow::{Context, Result};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, Mutex};
struct PendingWifiResponse {
version: u64,
payload: Option<String>,
}
pub(crate) struct HttpState {
wifi_response: Mutex<PendingWifiResponse>,
wifi_response_cv: Condvar,
config: Mutex<Arc<AppConfig>>,
player_status: Mutex<crate::core::message::PlayerStatusData>,
ble_ready: AtomicBool,
}
impl HttpState {
fn new(config: Arc<AppConfig>) -> Self {
let player_status = crate::core::message::PlayerStatusData {
running: false,
paused: !config.playback.auto_start,
in_transition: false,
current_index: 0,
playlist_length: config.playlist.len(),
current_video: config.playlist.first().map(|item| item.id.clone()),
};
Self {
wifi_response: Mutex::new(PendingWifiResponse {
version: 0,
payload: None,
}),
wifi_response_cv: Condvar::new(),
config: Mutex::new(config),
player_status: Mutex::new(player_status),
ble_ready: AtomicBool::new(false),
}
}
fn publish_wifi_result(&self, payload: String) {
if let Ok(mut state) = self.wifi_response.lock() {
state.version += 1;
state.payload = Some(payload);
self.wifi_response_cv.notify_all();
}
}
pub(crate) fn config(&self) -> Arc<AppConfig> {
self.config
.lock()
.map(|config| Arc::clone(&config))
.expect("http config state poisoned")
}
fn replace_config(&self, config: Arc<AppConfig>) {
if let Ok(mut current) = self.config.lock() {
*current = Arc::clone(&config);
}
if let Ok(mut player_status) = self.player_status.lock() {
player_status.playlist_length = config.playlist.len();
if player_status.current_video.is_none() {
player_status.current_video = config.playlist.first().map(|item| item.id.clone());
}
}
}
pub(crate) fn player_status(&self) -> crate::core::message::PlayerStatusData {
self.player_status
.lock()
.map(|status| status.clone())
.expect("http player status state poisoned")
}
fn update_player_status(&self, status: crate::core::message::PlayerStatusData) {
if let Ok(mut current) = self.player_status.lock() {
*current = status;
}
}
pub(crate) fn ble_ready(&self) -> bool {
self.ble_ready.load(Ordering::SeqCst)
}
fn set_ble_ready(&self, ready: bool) {
self.ble_ready.store(ready, Ordering::SeqCst);
}
}
pub struct HttpPlugin {
ctx: Option<PluginContext>,
state: Option<Arc<HttpState>>,
}
impl HttpPlugin {
pub fn new() -> Self {
Self { ctx: None }
Self {
ctx: None,
state: None,
}
}
}
@@ -29,11 +124,86 @@ impl Plugin for HttpPlugin {
}
fn init(&mut self, ctx: PluginContext) -> Result<()> {
self.state = Some(Arc::new(HttpState::new(Arc::clone(&ctx.config))));
self.ctx = Some(ctx);
Ok(())
}
fn start(&mut self) -> Result<()> { Ok(()) }
fn handle_message(&mut self, _msg: Message) -> Result<()> { Ok(()) }
fn start(&mut self) -> Result<()> {
let ctx = self
.ctx
.as_ref()
.context("http plugin context is not initialized")?;
if !ctx.config.remote_control.enabled {
println!("[HttpPlugin] Remote control disabled, skip HTTP server startup");
return Ok(());
}
let host = ctx.config.remote_control.host.clone();
let port = ctx.config.remote_control.port;
let tx = ctx.tx.clone();
let state = Arc::clone(
self.state
.as_ref()
.context("http plugin state is not initialized")?,
);
std::thread::spawn(move || {
let runtime = match tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
{
Ok(runtime) => runtime,
Err(error) => {
eprintln!("[HttpPlugin] failed to create tokio runtime: {error}");
return;
}
};
runtime.block_on(async move {
let routes = routes::build_routes(tx.clone(), state);
let addr: std::net::SocketAddr = match format!("{host}:{port}").parse() {
Ok(addr) => addr,
Err(error) => {
eprintln!("[HttpPlugin] invalid listen address {host}:{port}: {error}");
return;
}
};
if let Err(error) = tx.send(Envelope {
from: "http",
to: crate::core::message::Destination::Manager,
message: Message::PluginReady("http"),
}) {
eprintln!("[HttpPlugin] failed to report ready state: {error}");
}
println!("[HttpPlugin] listening on http://{addr}");
warp::serve(routes).run(addr).await;
});
});
Ok(())
}
fn handle_message(&mut self, msg: Message) -> Result<()> {
let state = match self.state.as_ref() {
Some(state) => state,
None => return Ok(()),
};
match msg {
Message::WifiResult(payload) => state.publish_wifi_result(payload),
Message::PlayerStatus(status) => state.update_player_status(status),
Message::ConfigReloaded(config) => state.replace_config(config),
Message::PluginReady("ble") => state.set_ble_ready(true),
Message::Shutdown => state.set_ble_ready(false),
_ => {}
}
Ok(())
}
fn stop(&mut self) -> Result<()> { Ok(()) }
}

412
src/plugins/http/routes.rs Normal file
View File

@@ -0,0 +1,412 @@
use super::HttpState;
use crate::core::config;
use crate::core::message::{Destination, Envelope, Message, PlayerCommand, WifiCommand};
use serde::Deserialize;
use serde::Serialize;
use serde_json::json;
use std::convert::Infallible;
use std::sync::{mpsc, Arc};
use std::time::{Duration, Instant};
use warp::http::StatusCode;
use warp::{Filter, Reply};
#[derive(Deserialize)]
struct WifiConnectRequest {
ssid: String,
password: String,
}
#[derive(Deserialize)]
struct WifiApStartRequest {
ssid: String,
password: String,
}
#[derive(Serialize)]
struct ApiMessage<'a> {
status: &'a str,
message: String,
}
pub(crate) fn build_routes(
tx: mpsc::Sender<Envelope>,
state: Arc<HttpState>,
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
let api = play_route(tx.clone())
.or(pause_route(tx.clone()))
.or(next_route(tx.clone()))
.or(previous_route(tx.clone()))
.or(goto_route(tx.clone()))
.or(trigger_route(tx.clone()))
.or(scene_route(tx.clone()))
.or(status_route(Arc::clone(&state)))
.or(config_get_route(Arc::clone(&state)))
.or(config_post_route(tx.clone(), Arc::clone(&state)))
.or(wifi_status_route(tx.clone(), Arc::clone(&state)))
.or(wifi_scan_route(tx.clone(), Arc::clone(&state)))
.or(wifi_connect_route(tx.clone(), Arc::clone(&state)))
.or(wifi_ap_start_route(tx.clone(), Arc::clone(&state)))
.or(wifi_ap_stop_route(tx, state));
let cors = warp::cors()
.allow_any_origin()
.allow_headers(["content-type"])
.allow_methods(["GET", "POST", "OPTIONS"]);
root_route().or(api).with(cors)
}
fn root_route() -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
warp::path::end().and(warp::get()).map(|| {
warp::reply::html(
"<!doctype html><html><head><meta charset=\"utf-8\"><title>ShowenV2 HTTP API</title></head><body><h1>ShowenV2 HTTP API</h1><p>HTTP API is running.</p></body></html>",
)
})
}
fn play_route(
tx: mpsc::Sender<Envelope>,
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
warp::path!("api" / "play")
.and(warp::post())
.and(with_tx(tx))
.and_then(|tx| command_reply(tx, Message::PlayerCommand(PlayerCommand::Play), "开始播放"))
}
fn pause_route(
tx: mpsc::Sender<Envelope>,
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
warp::path!("api" / "pause")
.and(warp::post())
.and(with_tx(tx))
.and_then(|tx| command_reply(tx, Message::PlayerCommand(PlayerCommand::Pause), "已暂停"))
}
fn next_route(
tx: mpsc::Sender<Envelope>,
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
warp::path!("api" / "next")
.and(warp::post())
.and(with_tx(tx))
.and_then(|tx| command_reply(tx, Message::PlayerCommand(PlayerCommand::Next), "切换到下一个视频"))
}
fn previous_route(
tx: mpsc::Sender<Envelope>,
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
warp::path!("api" / "previous")
.and(warp::post())
.and(with_tx(tx))
.and_then(|tx| command_reply(tx, Message::PlayerCommand(PlayerCommand::Previous), "切换到上一个视频"))
}
fn goto_route(
tx: mpsc::Sender<Envelope>,
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
warp::path!("api" / "goto" / usize)
.and(warp::post())
.and(with_tx(tx))
.and_then(|index, tx| {
command_reply(
tx,
Message::PlayerCommand(PlayerCommand::Goto(index)),
format!("跳转到视频 {index}"),
)
})
}
fn trigger_route(
tx: mpsc::Sender<Envelope>,
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
warp::path!("api" / "trigger" / String / String)
.and(warp::post())
.and(with_tx(tx))
.and_then(|name, value, tx| {
let message = format!("触发器 '{name}' 已发送,值: {value}");
command_reply(tx, Message::Trigger { name, value }, message)
})
}
fn scene_route(
tx: mpsc::Sender<Envelope>,
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
warp::path!("api" / "scene" / String)
.and(warp::post())
.and(with_tx(tx))
.and_then(|name, tx| {
let message = format!("切换到场景: {name}");
command_reply(tx, Message::PlayerCommand(PlayerCommand::ChangeScene(name)), message)
})
}
fn status_route(
state: Arc<HttpState>,
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
warp::path!("api" / "status")
.and(warp::get())
.and(with_state(state))
.and_then(status_reply)
}
fn config_get_route(
state: Arc<HttpState>,
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
warp::path!("api" / "config")
.and(warp::get())
.and(with_state(state))
.and_then(config_get_reply)
}
fn config_post_route(
tx: mpsc::Sender<Envelope>,
state: Arc<HttpState>,
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
warp::path!("api" / "config")
.and(warp::post())
.and(warp::body::content_length_limit(1024 * 64))
.and(warp::body::bytes())
.and(with_tx(tx))
.and(with_state(state))
.and_then(handle_config_post)
}
fn wifi_status_route(
tx: mpsc::Sender<Envelope>,
state: Arc<HttpState>,
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
warp::path!("api" / "wifi" / "status")
.and(warp::get())
.and(with_tx(tx))
.and(with_state(state))
.and_then(|tx, state| wifi_reply(tx, state, WifiCommand::Status))
}
fn wifi_scan_route(
tx: mpsc::Sender<Envelope>,
state: Arc<HttpState>,
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
warp::path!("api" / "wifi" / "scan")
.and(warp::get())
.and(with_tx(tx))
.and(with_state(state))
.and_then(|tx, state| wifi_reply(tx, state, WifiCommand::Scan))
}
fn wifi_connect_route(
tx: mpsc::Sender<Envelope>,
state: Arc<HttpState>,
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
warp::path!("api" / "wifi" / "connect")
.and(warp::post())
.and(warp::body::json())
.and(with_tx(tx))
.and(with_state(state))
.and_then(|req: WifiConnectRequest, tx, state| {
wifi_reply(
tx,
state,
WifiCommand::Connect {
ssid: req.ssid,
password: req.password,
},
)
})
}
fn wifi_ap_start_route(
tx: mpsc::Sender<Envelope>,
state: Arc<HttpState>,
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
warp::path!("api" / "wifi" / "ap" / "start")
.and(warp::post())
.and(warp::body::json())
.and(with_tx(tx))
.and(with_state(state))
.and_then(|req: WifiApStartRequest, tx, state| {
wifi_reply(
tx,
state,
WifiCommand::ApStart {
ssid: req.ssid,
password: req.password,
},
)
})
}
fn wifi_ap_stop_route(
tx: mpsc::Sender<Envelope>,
state: Arc<HttpState>,
) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone {
warp::path!("api" / "wifi" / "ap" / "stop")
.and(warp::post())
.and(with_tx(tx))
.and(with_state(state))
.and_then(|tx, state| wifi_reply(tx, state, WifiCommand::ApStop))
}
async fn status_reply(state: Arc<HttpState>) -> Result<warp::reply::Response, Infallible> {
Ok(warp::reply::json(&json!({
"player": state.player_status(),
"ble_ready": state.ble_ready(),
}))
.into_response())
}
async fn config_get_reply(state: Arc<HttpState>) -> Result<warp::reply::Response, Infallible> {
Ok(warp::reply::json(state.config().as_ref()).into_response())
}
async fn handle_config_post(
body: bytes::Bytes,
tx: mpsc::Sender<Envelope>,
state: Arc<HttpState>,
) -> Result<warp::reply::Response, Infallible> {
let current_config = state.config();
let raw = match std::str::from_utf8(&body) {
Ok(raw) => raw,
Err(_) => return Ok(error_json(StatusCode::BAD_REQUEST, "请求体不是有效的 UTF-8")),
};
if let Err(error) = config::parse_str(raw, &current_config.source_path) {
return Ok(error_json(
StatusCode::BAD_REQUEST,
&format!("配置验证失败: {error}"),
));
}
if let Err(error) = std::fs::write(&current_config.source_path, raw) {
return Ok(error_json(
StatusCode::INTERNAL_SERVER_ERROR,
&format!("写入配置文件失败: {error}"),
));
}
if let Err(error) = tx.send(Envelope {
from: "http",
to: Destination::Manager,
message: Message::ConfigReloadRequest,
}) {
return Ok(error_json(
StatusCode::INTERNAL_SERVER_ERROR,
&format!("发送配置重载请求失败: {error}"),
));
}
Ok(success_json("配置已保存并请求重载"))
}
async fn command_reply(
tx: mpsc::Sender<Envelope>,
message: Message,
success_message: impl Into<String>,
) -> Result<warp::reply::Response, Infallible> {
match tx.send(Envelope {
from: "http",
to: Destination::Plugin("video"),
message,
}) {
Ok(()) => Ok(success_json(success_message)),
Err(error) => Ok(error_json(
StatusCode::INTERNAL_SERVER_ERROR,
&format!("发送命令失败: {error}"),
)),
}
}
async fn wifi_reply(
tx: mpsc::Sender<Envelope>,
state: Arc<HttpState>,
command: WifiCommand,
) -> Result<warp::reply::Response, Infallible> {
let version = match state.wifi_response.lock() {
Ok(guard) => guard.version,
Err(_) => {
return Ok(error_json(
StatusCode::INTERNAL_SERVER_ERROR,
"WiFi 响应状态锁已损坏",
));
}
};
if let Err(error) = tx.send(Envelope {
from: "http",
to: Destination::Plugin("wifi"),
message: Message::WifiCommand(command),
}) {
return Ok(error_json(
StatusCode::INTERNAL_SERVER_ERROR,
&format!("发送 WiFi 命令失败: {error}"),
));
}
let deadline = Instant::now() + Duration::from_secs(10);
let mut guard = match state.wifi_response.lock() {
Ok(guard) => guard,
Err(_) => {
return Ok(error_json(
StatusCode::INTERNAL_SERVER_ERROR,
"WiFi 响应状态锁已损坏",
));
}
};
while guard.version == version {
let now = Instant::now();
if now >= deadline {
return Ok(error_json(StatusCode::GATEWAY_TIMEOUT, "等待 WiFi 响应超时"));
}
let timeout = deadline.saturating_duration_since(now);
let (next_guard, wait_result) = match state.wifi_response_cv.wait_timeout(guard, timeout) {
Ok(result) => result,
Err(_) => {
return Ok(error_json(
StatusCode::INTERNAL_SERVER_ERROR,
"等待 WiFi 响应失败",
));
}
};
guard = next_guard;
if wait_result.timed_out() && guard.version == version {
return Ok(error_json(StatusCode::GATEWAY_TIMEOUT, "等待 WiFi 响应超时"));
}
}
Ok(warp::reply::with_status(guard.payload.clone().unwrap_or_default(), StatusCode::OK).into_response())
}
fn with_tx(
tx: mpsc::Sender<Envelope>,
) -> impl Filter<Extract = (mpsc::Sender<Envelope>,), Error = Infallible> + Clone {
warp::any().map(move || tx.clone())
}
fn with_state(
state: Arc<HttpState>,
) -> impl Filter<Extract = (Arc<HttpState>,), Error = Infallible> + Clone {
warp::any().map(move || Arc::clone(&state))
}
fn success_json(message: impl Into<String>) -> warp::reply::Response {
warp::reply::with_status(
warp::reply::json(&ApiMessage {
status: "ok",
message: message.into(),
}),
StatusCode::OK,
)
.into_response()
}
fn error_json(status: StatusCode, message: &str) -> warp::reply::Response {
warp::reply::with_status(
warp::reply::json(&json!({
"status": "error",
"message": message,
})),
status,
)
.into_response()
}