batch E: close tail checks and refresh smoke evidence
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user