集成 OpenHub 与 WeKnora Page AI

This commit is contained in:
Agent Board
2026-06-26 20:01:02 +08:00
parent dabaf03bd7
commit e433c07061
63 changed files with 12678 additions and 332 deletions
+38 -4
View File
@@ -1,6 +1,7 @@
use crate::app::AppState;
use crate::context::RequestContext;
use crate::error::WebError;
use crate::provider_identity_sync::sync_provider_identities;
use crate::routes::local_folder_source::{
control_plane_db_path_display, create_default_local_workspace_for_actor,
ensure_local_workspace_read_access_with_state, is_local_access_policy_admin_context,
@@ -136,7 +137,7 @@ pub async fn auth_api(
return Ok(build_sqlite_sign_out_response(&state, &context));
}
handle_sqlite_auth_action(&state, &context, &payload)
handle_sqlite_auth_action(&state, &context, &payload).await
}
pub async fn auth_entry(
@@ -2069,7 +2070,7 @@ fn has_real_auth_context(state: &AppState, context: &RequestContext) -> bool {
|| extract_cookie_value(context, COOKIE_MNOTE_WEB_CONVEX_TOKEN).is_some()
}
fn handle_sqlite_auth_action(
async fn handle_sqlite_auth_action(
state: &AppState,
context: &RequestContext,
payload: &serde_json::Value,
@@ -2098,6 +2099,7 @@ fn handle_sqlite_auth_action(
})?;
let session_token = new_session_token();
let token_hash = session_token_hash(&session_token);
let password_for_provider_sync = password.clone();
let resolved = if flow == "signUp" {
let email = params
.get("email")
@@ -2200,14 +2202,46 @@ fn handle_sqlite_auth_action(
.unwrap_or_else(|_| "{}".to_string()),
});
Ok(build_sqlite_auth_response(
let provider_sync_results = if flow == "signUp" {
let results = sync_provider_identities(&resolved.user, &password_for_provider_sync).await;
let _ = state.control_plane().append_audit(AppendAuditInput {
actor_user_id: Some(resolved.user.id.clone()),
action: "control.auth.provider_identities_synced".to_string(),
target_kind: "user".to_string(),
target_id: Some(resolved.user.id.clone()),
metadata_json: serde_json::to_string(&json!({
"providers": results.iter().map(|item| json!({
"provider": item.provider,
"ok": item.ok,
"message": item.message,
"providerUserId": item.provider_user_id,
})).collect::<Vec<_>>(),
}))
.unwrap_or_else(|_| "{}".to_string()),
});
Some(results)
} else {
None
};
let mut response = build_sqlite_auth_response(
context,
&session_token,
&resolved.user.id,
resolved.user.email.as_deref().unwrap_or_default(),
&resolved.user.display_name,
&effective_sqlite_auth_actor_type(&resolved.user.id, &resolved.user.role),
))
);
if let Some(results) = provider_sync_results {
let all_ok = results.iter().all(|item| item.ok);
let header_value = if all_ok { "ok" } else { "partial" };
if let Ok(value) = HeaderValue::from_str(header_value) {
response
.headers_mut()
.insert("x-mnote-provider-identity-sync", value);
}
}
Ok(response)
}
fn sqlite_auth_error(
@@ -368,11 +368,21 @@ pub(crate) async fn execute_mnote_tool_call(
Err(WebError::new(
StatusCode::GONE,
"mnote_evidence_tools_retired",
"旧 docs/evidence/LiteParse tools 已退役;请使用 mnote.knowledge_rag.query/open_reference",
"旧 docs/evidence/LiteParse tools 已退役;请使用 mnote.weknora.search/open_reference 或兼容 mnote.knowledge_rag.query/open_reference",
)
.with_context(&context))
}
"mnote.knowledge_rag.status" => knowledge_rag::status(&state, &context, &input).await,
"mnote.weknora.search" => knowledge_rag::search(&state, &context, &input).await,
"mnote.weknora.list_sources" => {
knowledge_rag::list_sources(&state, &context, &input).await
}
"mnote.weknora.get_source_status" => {
knowledge_rag::get_source_status(&state, &context, &input).await
}
"mnote.weknora.open_reference" => {
knowledge_rag::open_reference(&state, &context, &input).await
}
"mnote.knowledge_rag.query" => knowledge_rag::query(&state, &context, &input).await,
"mnote.knowledge_rag.section_context" => {
knowledge_rag::section_context(&state, &context, &input).await
@@ -384,7 +394,7 @@ pub(crate) async fn execute_mnote_tool_call(
WebError::new(
StatusCode::GONE,
"mnote_index_tools_retired",
"旧本地索引 tools 已退役;资料索引统一由 LightRAG provider 处理",
"旧本地索引 tools 已退役;资料索引统一由当前知识库 provider 处理",
)
.with_context(&context),
),
@@ -728,6 +738,10 @@ fn is_read_tool(tool_name: &str) -> bool {
| "mnote.evidence.read"
| "mnote.evidence.open"
| "mnote.knowledge_rag.status"
| "mnote.weknora.search"
| "mnote.weknora.list_sources"
| "mnote.weknora.get_source_status"
| "mnote.weknora.open_reference"
| "mnote.knowledge_rag.query"
| "mnote.knowledge_rag.section_context"
| "mnote.knowledge_rag.open_reference"
@@ -761,6 +775,10 @@ fn is_evidence_receipt_tool(tool_name: &str) -> bool {
| "mnote.evidence.read"
| "mnote.evidence.open"
| "mnote.knowledge_rag.status"
| "mnote.weknora.search"
| "mnote.weknora.list_sources"
| "mnote.weknora.get_source_status"
| "mnote.weknora.open_reference"
| "mnote.knowledge_rag.query"
| "mnote.knowledge_rag.section_context"
| "mnote.knowledge_rag.open_reference"
File diff suppressed because it is too large Load Diff
+99 -8
View File
@@ -26,6 +26,7 @@ mod onlyoffice;
pub(crate) mod onlyoffice_bridge;
mod page_ai_board;
mod page_ai_opencode;
mod page_ai_openhub;
mod page_ai_workflow;
mod query_support;
mod resource_trash;
@@ -91,6 +92,10 @@ pub fn build_router(state: AppState) -> Router {
"/api/knowledge-rag/pipeline-events",
get(knowledge_rag::pipeline_events),
)
.route(
"/api/knowledge-rag/knowledge-bases",
post(knowledge_rag::create_knowledge_base),
)
.route("/api/knowledge-rag/ingest", post(knowledge_rag::ingest))
.route("/api/knowledge-rag/search", post(knowledge_rag::search))
.route("/api/knowledge-rag/query", post(knowledge_rag::query_rag))
@@ -389,6 +394,68 @@ pub fn build_router(state: AppState) -> Router {
"/api/page-ai/opencode/status",
get(page_ai_opencode::status),
)
.route("/api/page-ai/openhub/status", get(page_ai_openhub::status))
.route(
"/api/page-ai/openhub/bootstrap",
post(page_ai_openhub::bootstrap),
)
.route(
"/api/page-ai/openhub/artifact-index",
get(page_ai_openhub::artifact_index_get).post(page_ai_openhub::artifact_index_upsert),
)
.route("/page-ai/openhub/ai", get(page_ai_openhub::ai_shell))
.route(
"/page-ai/openhub/ai/{*path}",
any(page_ai_openhub::ai_proxy),
)
.route(
"/page-ai/openhub/login",
any(page_ai_openhub::non_ai_route_guard),
)
.route(
"/page-ai/openhub/login/{*path}",
any(page_ai_openhub::non_ai_route_guard),
)
.route(
"/page-ai/openhub/admin",
any(page_ai_openhub::non_ai_route_guard),
)
.route(
"/page-ai/openhub/admin/{*path}",
any(page_ai_openhub::non_ai_route_guard),
)
.route(
"/page-ai/openhub/file",
any(page_ai_openhub::non_ai_route_guard),
)
.route(
"/page-ai/openhub/file/{*path}",
any(page_ai_openhub::non_ai_route_guard),
)
.route(
"/page-ai/openhub/files",
any(page_ai_openhub::non_ai_route_guard),
)
.route(
"/page-ai/openhub/files/{*path}",
any(page_ai_openhub::non_ai_route_guard),
)
.route(
"/page-ai/openhub/knowledge",
any(page_ai_openhub::non_ai_route_guard),
)
.route(
"/page-ai/openhub/knowledge/{*path}",
any(page_ai_openhub::non_ai_route_guard),
)
.route(
"/page-ai/openhub/git",
any(page_ai_openhub::non_ai_route_guard),
)
.route(
"/page-ai/openhub/git/{*path}",
any(page_ai_openhub::non_ai_route_guard),
)
.route(
"/api/page-ai/opencode/session",
post(page_ai_opencode::bind_session),
@@ -452,16 +519,31 @@ pub fn build_router(state: AppState) -> Router {
.route("/page-ai/opencode/{*path}", any(page_ai_opencode::proxy))
.route("/assets/{*path}", any(page_ai_opencode::proxy_assets))
.route("/global/{*path}", any(page_ai_opencode::proxy_assets))
.route("/favicon-96x96-v3.png", any(page_ai_opencode::proxy_current_path))
.route(
"/favicon-96x96-v3.png",
any(page_ai_opencode::proxy_current_path),
)
.route("/favicon-v3.svg", any(page_ai_opencode::proxy_current_path))
.route("/favicon-v3.ico", any(page_ai_opencode::proxy_current_path))
.route("/apple-touch-icon-v3.png", any(page_ai_opencode::proxy_current_path))
.route("/site.webmanifest", any(page_ai_opencode::proxy_current_path))
.route("/social-share.png", any(page_ai_opencode::proxy_current_path))
.route(
"/apple-touch-icon-v3.png",
any(page_ai_opencode::proxy_current_path),
)
.route(
"/site.webmanifest",
any(page_ai_opencode::proxy_current_path),
)
.route(
"/social-share.png",
any(page_ai_opencode::proxy_current_path),
)
.route("/provider", any(page_ai_opencode::proxy_current_path))
.route("/path", any(page_ai_opencode::proxy_current_path))
.route("/project", any(page_ai_opencode::proxy_current_path))
.route("/project/{*path}", any(page_ai_opencode::proxy_current_path))
.route(
"/project/{*path}",
any(page_ai_opencode::proxy_current_path),
)
.route("/lsp", any(page_ai_opencode::proxy_current_path))
.route("/command", any(page_ai_opencode::proxy_current_path))
.route("/mcp", any(page_ai_opencode::proxy_current_path))
@@ -472,10 +554,19 @@ pub fn build_router(state: AppState) -> Router {
.route("/question", any(page_ai_opencode::proxy_current_path))
.route("/event", any(page_ai_opencode::proxy_current_path))
.route("/session", any(page_ai_opencode::proxy_current_path))
.route("/session/{*path}", any(page_ai_opencode::proxy_current_path))
.route(
"/session/{*path}",
any(page_ai_opencode::proxy_current_path),
)
.route("/new-session", any(page_ai_opencode::proxy_current_path))
.route("/{opencode_dir}/session", any(page_ai_opencode::proxy_current_path))
.route("/{opencode_dir}/session/{*path}", any(page_ai_opencode::proxy_current_path))
.route(
"/{opencode_dir}/session",
any(page_ai_opencode::proxy_current_path),
)
.route(
"/{opencode_dir}/session/{*path}",
any(page_ai_opencode::proxy_current_path),
)
.route("/api/page-ai/board/status", get(page_ai_board::status))
.route("/api/page-ai/board/workers", get(page_ai_board::workers))
.route(
@@ -142,7 +142,9 @@ fn file_uri_to_path(root_uri: &str) -> Option<String> {
Some(percent_decode_path_lossy(&normalized))
}
fn opencode_project_directory_for_request(request: Option<&OpencodeSessionRequest>) -> Option<String> {
fn opencode_project_directory_for_request(
request: Option<&OpencodeSessionRequest>,
) -> Option<String> {
if let Some(override_directory) = std::env::var("MNOTE_OPENCODE_PROJECT_DIR")
.ok()
.map(|value| value.trim().to_string())
@@ -1025,6 +1027,23 @@ async fn proxy_to_opencode(
body: Body,
) -> Result<Response, WebError> {
ensure_authenticated(&context)?;
if std::env::var("MNOTE_PAGE_AI_OPENCODE_LEGACY_PROXY")
.ok()
.map(|value| value.trim() == "1")
.unwrap_or(false)
!= true
{
return Response::builder()
.status(StatusCode::GONE)
.header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
.body(Body::from("page_ai_opencode_legacy_proxy_disabled"))
.map_err(|error| {
WebError::internal(format!(
"opencode legacy proxy disabled response 构造失败: {error}"
))
.with_context(&context)
});
}
let client =
opencode_client(Duration::from_secs(120)).map_err(|error| error.with_context(&context))?;
let url = upstream_url(&path, uri.query()).map_err(|error| error.with_context(&context))?;
File diff suppressed because it is too large Load Diff