batch E: close tail checks and refresh smoke evidence

This commit is contained in:
lix-2026
2026-05-21 17:15:28 +08:00
parent 1e8f5c5816
commit cc91a56603
16 changed files with 1093 additions and 45 deletions
@@ -1,5 +1,6 @@
use crate::app::AppState;
use crate::context::RequestContext;
use crate::document_buffer_store::{self, BufferKey, BufferStore};
use crate::error::WebError;
use crate::routes::command_support::{
execute_runtime_command_via_convex, execute_runtime_command_via_convex_with_artifacts,
@@ -38,6 +39,64 @@ pub struct DocumentMetaQuery {
pub workspace_id: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BufferStateQuery {
pub document_id: Option<String>,
pub workspace_id: Option<String>,
pub source_kind: Option<String>,
pub root_uri: Option<String>,
pub relative_path: Option<String>,
}
fn find_document_buffer_state(
buffer_store: &BufferStore,
query: &BufferStateQuery,
) -> Option<core_protocol::DocumentBuffer> {
if let (Some(root_uri), Some(_source_kind), Some(document_id)) = (
query.root_uri.as_deref().filter(|v| !v.is_empty()),
query.source_kind.as_deref().filter(|v| !v.is_empty()),
query.document_id.as_deref().filter(|v| !v.is_empty()),
) {
// 本地文件夹查询优先用明确 relativePath;缺省时退回扫描,避免前端只知道 documentId 时查不到已初始化 buffer。
if let Some(relative_path) = query.relative_path.as_deref().filter(|v| !v.is_empty()) {
let ws_path = document_buffer_store::build_local_folder_workspace_path(
query.workspace_id.as_deref().unwrap_or(""),
root_uri,
relative_path,
document_id,
);
return buffer_store.get_by_path(&ws_path);
}
return buffer_store.all_buffers().into_iter().find(|buf| {
buf.workspace_path.root_uri == root_uri
&& buf.workspace_path.object_identity.document_id.as_deref() == Some(document_id)
});
}
if let (Some(document_id), Some(workspace_id), Some(relative_path)) = (
query.document_id.as_deref().filter(|v| !v.is_empty()),
query.workspace_id.as_deref().filter(|v| !v.is_empty()),
query.relative_path.as_deref().filter(|v| !v.is_empty()),
) {
// 通过完整 workspace path 精确查询。
let key = BufferKey::from_parts(
workspace_id,
"LocalFolder",
"",
relative_path,
Some(document_id.to_string()),
);
return buffer_store.get(&key);
}
if let Some(document_id) = query.document_id.as_deref().filter(|v| !v.is_empty()) {
// 最后按 documentId 扫描,作为调试和兼容查询兜底。
return buffer_store.all_buffers().into_iter().find(|buf| {
buf.workspace_path.object_identity.document_id.as_deref() == Some(document_id)
});
}
None
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DocumentSaveRequest {
@@ -520,6 +579,42 @@ pub async fn content(
Ok(ok_response(&context, result))
}
/// 查询指定文档的当前 BufferStore 状态。
///
/// 支持两种查询方式:
/// 1. 通过 documentId + sourceKind + rootUri 自动构造 workspace path
/// 2. 通过 documentId + workspaceId + relativePath 精确查询
pub async fn buffer_state(
State(state): State<AppState>,
Extension(context): Extension<RequestContext>,
Query(query): Query<BufferStateQuery>,
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
let result = find_document_buffer_state(&state.buffer_store, &query);
match result {
Some(buf) => Ok(ok_response(
&context,
json!({
"documentId": buf.workspace_path.object_identity.document_id,
"workspaceId": buf.workspace_path.workspace_id,
"dirtyState": format!("{:?}", buf.dirty_state),
"fileVersion": buf.file_version,
"baseContentHash": buf.base_content_hash,
"currentContentHash": buf.current_content_hash,
"externalActor": buf.external_actor,
"lastLoadedAt": buf.last_loaded_at,
"lastSavedAt": buf.last_saved_at,
}),
)),
None => Err(WebError::new(
StatusCode::NOT_FOUND,
"buffer_state_not_found",
"未找到该文档的 buffer 状态",
)
.with_context(&context)),
}
}
pub async fn page_body_write(
State(state): State<AppState>,
Extension(context): Extension<RequestContext>,
@@ -1001,6 +1096,7 @@ pub async fn options(
#[cfg(test)]
mod tests {
use crate::app::{build_app, AppConfig, AppState};
use crate::document_buffer_store::{build_local_folder_workspace_path, BufferStore};
use axum::body::{to_bytes, Body};
use axum::http::{HeaderMap, HeaderValue, Method, Request, StatusCode, Uri};
use serde_json::Value;
@@ -1103,6 +1199,30 @@ mod tests {
}))
}
#[test]
fn documents_buffer_state_falls_back_to_document_id_without_relative_path() {
let store = BufferStore::new();
let ws_path = build_local_folder_workspace_path(
"ws_local",
"file:///tmp/mnote-root",
"docs/page.md",
"local-md:docs~2Fpage.md",
);
store.init_buffer(&ws_path, Some("version-1".into()), Some("hash-1".into()));
let query = super::BufferStateQuery {
document_id: Some("local-md:docs~2Fpage.md".into()),
workspace_id: Some("ws_local".into()),
source_kind: Some("local_folder".into()),
root_uri: Some("file:///tmp/mnote-root".into()),
relative_path: None,
};
let result = super::find_document_buffer_state(&store, &query).expect("buffer state");
assert_eq!(result.file_version.as_deref(), Some("version-1"));
assert_eq!(result.workspace_path.relative_path, "docs/page.md");
}
#[test]
fn convex_auth_cookie_does_not_force_next_proxy() {
let mut headers = HeaderMap::new();