230 lines
7.0 KiB
Rust
230 lines
7.0 KiB
Rust
use crate::app::AppState;
|
|||
|
|
use crate::context::RequestContext;
|
||
|
|
use crate::error::WebError;
|
||
|
|
use crate::hermes_tools::{ensure_write_authorized, ToolCallInput};
|
||
|
|
use crate::routes;
|
||
|
|
use axum::http::StatusCode;
|
||
|
|
use serde::Deserialize;
|
||
|
|
use serde_json::{json, Value};
|
||
|
|
|
||
|
|
#[derive(Debug, Deserialize)]
|
||
|
|
#[serde(rename_all = "camelCase")]
|
||
|
|
struct IndexSettingsArgs {
|
||
|
|
#[serde(default)]
|
||
|
|
include_paths: Vec<String>,
|
||
|
|
#[serde(default)]
|
||
|
|
schedule_mode: Option<String>,
|
||
|
|
#[serde(default)]
|
||
|
|
schedule_time: Option<String>,
|
||
|
|
#[serde(default)]
|
||
|
|
schedule_date: Option<String>,
|
||
|
|
#[serde(default)]
|
||
|
|
run_on_change: Option<bool>,
|
||
|
|
}
|
||
|
|
|
||
|
|
pub async fn index_status(
|
||
|
|
state: &AppState,
|
||
|
|
context: &RequestContext,
|
||
|
|
input: &ToolCallInput,
|
||
|
|
) -> Result<Value, WebError> {
|
||
|
|
let workspace_id = effective_workspace_id(context, input);
|
||
|
|
let root_uri = required_root_uri(
|
||
|
|
context,
|
||
|
|
input,
|
||
|
|
"mnote_index_root_required",
|
||
|
|
"本地索引状态缺少 rootUri",
|
||
|
|
)?;
|
||
|
|
let root_path =
|
||
|
|
routes::ensure_local_workspace_read_access_with_state(state, context, &root_uri)
|
||
|
|
.map_err(|error| error.with_context(context))?;
|
||
|
|
let user_settings = read_user_settings(state, context, &workspace_id, &root_path)?;
|
||
|
|
let effective_settings = routes::effective_local_index_settings_for_root(
|
||
|
|
state.control_plane(),
|
||
|
|
&workspace_id,
|
||
|
|
&root_path,
|
||
|
|
)?;
|
||
|
|
let status = routes::local_index_status_with_settings(
|
||
|
|
&root_path,
|
||
|
|
&root_uri,
|
||
|
|
&workspace_id,
|
||
|
|
&user_settings,
|
||
|
|
&effective_settings,
|
||
|
|
)?;
|
||
|
|
Ok(json!({
|
||
|
|
"ok": true,
|
||
|
|
"schema": "mnote.index.status_result.v1",
|
||
|
|
"result": status
|
||
|
|
}))
|
||
|
|
}
|
||
|
|
|
||
|
|
pub async fn index_refresh(
|
||
|
|
state: &AppState,
|
||
|
|
context: &RequestContext,
|
||
|
|
input: &ToolCallInput,
|
||
|
|
) -> Result<Value, WebError> {
|
||
|
|
let workspace_id = effective_workspace_id(context, input);
|
||
|
|
let root_uri = required_root_uri(
|
||
|
|
context,
|
||
|
|
input,
|
||
|
|
"mnote_index_root_required",
|
||
|
|
"本地索引刷新缺少 rootUri",
|
||
|
|
)?;
|
||
|
|
let root_path =
|
||
|
|
routes::ensure_local_workspace_read_access_with_state(state, context, &root_uri)
|
||
|
|
.map_err(|error| error.with_context(context))?;
|
||
|
|
let effective_settings = routes::effective_local_index_settings_for_root(
|
||
|
|
state.control_plane(),
|
||
|
|
&workspace_id,
|
||
|
|
&root_path,
|
||
|
|
)?;
|
||
|
|
let refreshed = routes::refresh_local_search_index_with_settings(
|
||
|
|
&root_path,
|
||
|
|
&root_uri,
|
||
|
|
&workspace_id,
|
||
|
|
&effective_settings,
|
||
|
|
)?;
|
||
|
|
Ok(json!({
|
||
|
|
"ok": true,
|
||
|
|
"schema": "mnote.index.refresh_result.v1",
|
||
|
|
"index": refreshed
|
||
|
|
}))
|
||
|
|
}
|
||
|
|
|
||
|
|
pub async fn index_update_settings(
|
||
|
|
state: &AppState,
|
||
|
|
context: &RequestContext,
|
||
|
|
input: &ToolCallInput,
|
||
|
|
) -> Result<Value, WebError> {
|
||
|
|
ensure_write_authorized(context, input)?;
|
||
|
|
let workspace_id = effective_workspace_id(context, input);
|
||
|
|
let root_uri = required_root_uri(
|
||
|
|
context,
|
||
|
|
input,
|
||
|
|
"mnote_index_root_required",
|
||
|
|
"本地索引设置缺少 rootUri",
|
||
|
|
)?;
|
||
|
|
let args = parse_settings_args(context, input)?;
|
||
|
|
let root_path =
|
||
|
|
routes::ensure_local_workspace_write_access_with_state(state, context, &root_uri)
|
||
|
|
.map_err(|error| error.with_context(context))?;
|
||
|
|
let actor_id = routes::current_actor_id(state, context).ok_or_else(|| {
|
||
|
|
WebError::new(
|
||
|
|
StatusCode::UNAUTHORIZED,
|
||
|
|
"local_index_settings_auth_required",
|
||
|
|
"本地索引设置需要登录用户",
|
||
|
|
)
|
||
|
|
.with_context(context)
|
||
|
|
})?;
|
||
|
|
if input.dry_run == Some(true) {
|
||
|
|
let planned_settings = routes::preview_user_local_index_settings(
|
||
|
|
state.control_plane(),
|
||
|
|
&actor_id,
|
||
|
|
&workspace_id,
|
||
|
|
&root_path,
|
||
|
|
&args.include_paths,
|
||
|
|
args.schedule_mode.as_deref(),
|
||
|
|
args.schedule_time.as_deref(),
|
||
|
|
args.schedule_date.as_deref(),
|
||
|
|
args.run_on_change,
|
||
|
|
)?;
|
||
|
|
let current_status = index_status(state, context, input).await?;
|
||
|
|
let would_clear_index_files = planned_settings.include_paths.is_empty();
|
||
|
|
return Ok(json!({
|
||
|
|
"ok": true,
|
||
|
|
"schema": "mnote.index.update_settings_plan.v1",
|
||
|
|
"dryRun": true,
|
||
|
|
"plannedSettings": planned_settings,
|
||
|
|
"currentStatus": current_status.get("result").cloned().unwrap_or(Value::Null),
|
||
|
|
"wouldRefresh": true,
|
||
|
|
"wouldClearIndexFiles": would_clear_index_files
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
let settings = routes::write_user_local_index_settings(
|
||
|
|
state.control_plane(),
|
||
|
|
&actor_id,
|
||
|
|
&workspace_id,
|
||
|
|
&root_path,
|
||
|
|
&args.include_paths,
|
||
|
|
args.schedule_mode.as_deref(),
|
||
|
|
args.schedule_time.as_deref(),
|
||
|
|
args.schedule_date.as_deref(),
|
||
|
|
args.run_on_change,
|
||
|
|
)?;
|
||
|
|
let effective_settings = routes::effective_local_index_settings_for_root(
|
||
|
|
state.control_plane(),
|
||
|
|
&workspace_id,
|
||
|
|
&root_path,
|
||
|
|
)?;
|
||
|
|
let refreshed = routes::refresh_local_search_index_with_settings(
|
||
|
|
&root_path,
|
||
|
|
&root_uri,
|
||
|
|
&workspace_id,
|
||
|
|
&effective_settings,
|
||
|
|
)?;
|
||
|
|
let status = routes::local_index_status_with_settings(
|
||
|
|
&root_path,
|
||
|
|
&root_uri,
|
||
|
|
&workspace_id,
|
||
|
|
&settings,
|
||
|
|
&effective_settings,
|
||
|
|
)?;
|
||
|
|
Ok(json!({
|
||
|
|
"ok": true,
|
||
|
|
"schema": "mnote.index.update_settings_result.v1",
|
||
|
|
"settings": settings,
|
||
|
|
"index": refreshed,
|
||
|
|
"result": status
|
||
|
|
}))
|
||
|
|
}
|
||
|
|
|
||
|
|
fn parse_settings_args(
|
||
|
|
context: &RequestContext,
|
||
|
|
input: &ToolCallInput,
|
||
|
|
) -> Result<IndexSettingsArgs, WebError> {
|
||
|
|
let args = input.args.clone().unwrap_or_else(|| json!({}));
|
||
|
|
serde_json::from_value::<IndexSettingsArgs>(args).map_err(|error| {
|
||
|
|
WebError::bad_request_code(
|
||
|
|
"mnote_index_settings_payload_invalid",
|
||
|
|
format!("本地索引设置参数无效: {error}"),
|
||
|
|
)
|
||
|
|
.with_context(context)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
fn read_user_settings(
|
||
|
|
state: &AppState,
|
||
|
|
context: &RequestContext,
|
||
|
|
workspace_id: &str,
|
||
|
|
root_path: &std::path::Path,
|
||
|
|
) -> Result<routes::LocalIndexSettings, WebError> {
|
||
|
|
if let Some(actor_id) = routes::current_actor_id(state, context) {
|
||
|
|
return routes::read_user_local_index_settings(
|
||
|
|
state.control_plane(),
|
||
|
|
&actor_id,
|
||
|
|
workspace_id,
|
||
|
|
root_path,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
routes::read_local_index_settings_or_default(root_path)
|
||
|
|
}
|
||
|
|
|
||
|
|
fn effective_workspace_id(context: &RequestContext, input: &ToolCallInput) -> String {
|
||
|
|
input
|
||
|
|
.effective_workspace_id()
|
||
|
|
.or_else(|| context.workspace.workspace_id.clone())
|
||
|
|
.unwrap_or_else(|| "default".into())
|
||
|
|
}
|
||
|
|
|
||
|
|
fn required_root_uri(
|
||
|
|
context: &RequestContext,
|
||
|
|
input: &ToolCallInput,
|
||
|
|
code: &'static str,
|
||
|
|
message: &'static str,
|
||
|
|
) -> Result<String, WebError> {
|
||
|
|
input
|
||
|
|
.effective_root_uri()
|
||
|
|
.map(|value| value.trim().to_string())
|
||
|
|
.filter(|value| !value.is_empty())
|
||
|
|
.ok_or_else(|| WebError::bad_request_code(code, message).with_context(context))
|
||
|
|
}
|