1e62d8317a
- 活动回合事实源统一到 Direct 线程管理器:删除 direct_runtime 的第二份快照,接单、进度内容变化与收口各广播一次活动回合变更事件
- 平台维护态判定移入 Rust 并在渲染层只订阅单一事件:新增 platform_maintenance 模块与各平台 facade 错误分支的分类入口
- 封面生成请求补 generationInputs.source,保持队列回填后仍能拿到平台素材 ID
- 渲染层按 master 5398a53e6 退役诊断详情入口:删除 agentRuntimeErrorDetail 与「查看详情」交互及其专属用例
- 冲突收口:nginx SPA 白名单、.gitignore、mobile 检查脚本、capabilities 描述取上游,两个已退役计划随上游删除,文档保留双方条目
- 新增并回写本里程碑取证、decision-log 与 pitfalls 的 2026-09-28 记录
91 lines
3.5 KiB
Rust
91 lines
3.5 KiB
Rust
//! 平台维护态在客户端的唯一出口。
|
||
//!
|
||
//! 渲染层不再自己解析 HTTP:维护响应由 [`crate::http_client::send_with_maintenance_watch`]
|
||
//! 在平台请求出口统一分类,再经本模块广播一次
|
||
//! `genarrative-client-maintenance-detected`。界面只订阅这个事件,因此同一批并发失败
|
||
//! 只会让同一个维护弹窗出现一次,各业务面板自己的错误文案保持不变。
|
||
|
||
use std::sync::OnceLock;
|
||
|
||
#[cfg(test)]
|
||
use std::sync::atomic::{AtomicU64, Ordering};
|
||
|
||
pub(crate) const PLATFORM_MAINTENANCE_EVENT: &str = "genarrative-client-maintenance-detected";
|
||
|
||
static PLATFORM_MAINTENANCE_APP_HANDLE: OnceLock<tauri::AppHandle> = OnceLock::new();
|
||
#[cfg(test)]
|
||
static PLATFORM_MAINTENANCE_EVENT_TEST_COUNT: AtomicU64 = AtomicU64::new(0);
|
||
|
||
pub(crate) fn set_platform_maintenance_app_handle(app: tauri::AppHandle) {
|
||
let _ = PLATFORM_MAINTENANCE_APP_HANDLE.set(app);
|
||
}
|
||
|
||
/// 判断一次平台响应是不是维护窗口。
|
||
///
|
||
/// 口径与渲染层此前的判定一致:只有 `503` 才算维护窗口,且响应体里要能看出
|
||
/// `code = MAINTENANCE` 或「维护」。普通 5xx、对象存储自己的 503(例如 `SlowDown`)
|
||
/// 和网关生成的 503 页面都不归类,避免把短暂抖动讲成系统维护。
|
||
pub(crate) fn response_is_maintenance(status: u16, body: &str) -> bool {
|
||
if status != 503 {
|
||
return false;
|
||
}
|
||
body.to_ascii_uppercase().contains("MAINTENANCE") || body.contains("维护")
|
||
}
|
||
|
||
/// 广播一次客户端维护事件;没有 AppHandle(单元测试、CLI)时只累计测试计数。
|
||
pub(crate) fn emit_platform_maintenance_notice(status: u16) {
|
||
#[cfg(test)]
|
||
PLATFORM_MAINTENANCE_EVENT_TEST_COUNT.fetch_add(1, Ordering::AcqRel);
|
||
let Some(app) = PLATFORM_MAINTENANCE_APP_HANDLE.get() else {
|
||
return;
|
||
};
|
||
let _ = tauri::Emitter::emit(
|
||
app,
|
||
PLATFORM_MAINTENANCE_EVENT,
|
||
serde_json::json!({ "code": "MAINTENANCE", "status": status }),
|
||
);
|
||
}
|
||
|
||
/// 在平台响应的错误分支上分类维护态:命中就广播一次,未命中什么都不做。
|
||
///
|
||
/// 调用方在错误分支里本来就会读取响应体来构造自己的错误文案,这里只复用同一份 body,
|
||
/// 既不改写任何错误语义,也不额外发请求。
|
||
pub(crate) fn watch_platform_response(status: u16, body: &str) {
|
||
if response_is_maintenance(status, body) {
|
||
emit_platform_maintenance_notice(status);
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
pub(crate) fn platform_maintenance_event_test_count() -> u64 {
|
||
PLATFORM_MAINTENANCE_EVENT_TEST_COUNT.load(Ordering::Acquire)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::response_is_maintenance;
|
||
|
||
#[test]
|
||
fn only_503_with_a_maintenance_body_is_a_maintenance_window() {
|
||
assert!(response_is_maintenance(
|
||
503,
|
||
r#"{"error":{"code":"MAINTENANCE","message":"服务正在维护"}}"#
|
||
));
|
||
assert!(response_is_maintenance(
|
||
503,
|
||
r#"{"error":{"message":"系统维护中"}}"#
|
||
));
|
||
// 只有 503 才算:其它状态码即使带 MAINTENANCE 也不弹维护窗。
|
||
assert!(!response_is_maintenance(
|
||
500,
|
||
r#"{"error":{"code":"MAINTENANCE"}}"#
|
||
));
|
||
// 503 但不是维护:对象存储的 SlowDown 与普通网关错误不能冒充维护。
|
||
assert!(!response_is_maintenance(
|
||
503,
|
||
r#"{"code":"SlowDown","message":"please reduce your request rate"}"#
|
||
));
|
||
assert!(!response_is_maintenance(503, ""));
|
||
}
|
||
}
|