Files
cursor-byok/server/src/cursor/services/knowledge/sync.rs
T
leookun e7a1cca4c6 feat: add group name functionality to models
- Introduced a new `group_name` field in the model configuration to allow for custom provider-group display names.
- Updated the `CursorModelCards`, `CursorModelEditor`, and `CursorSettingsPage` components to support group settings.
- Enhanced the UI to include group settings options, allowing users to modify group names and associated configurations.
- Added localization strings for new group settings features in both English and Chinese.
- Implemented a database migration to add the `group_name` column to the model configurations.
2026-08-30 23:28:05 +08:00

185 lines
5.9 KiB
Rust

//! Replays the offline journal to upstream and mirrors upstream list state.
use axum::{
body::{Body, Bytes},
http::{header, HeaderMap, HeaderValue, Method, Request},
};
use prost::Message;
use crate::{api::cursor::proxy, cursor::protocol::connect, Result};
use super::{
store::{JournalOp, RuleRecord, RuleStore},
KnowledgeBaseAddRequest, KnowledgeBaseAddResponse, KnowledgeBaseListItem,
KnowledgeBaseRemoveRequest, KnowledgeBaseRemoveResponse, KnowledgeBaseUpdateRequest,
KnowledgeBaseUpdateResponse,
};
const ADD_PATH: &str = "/aiserver.v1.AiService/KnowledgeBaseAdd";
const UPDATE_PATH: &str = "/aiserver.v1.AiService/KnowledgeBaseUpdate";
const REMOVE_PATH: &str = "/aiserver.v1.AiService/KnowledgeBaseRemove";
/// 逐条把离线日志推送到上游。返回 true 表示日志已清空(上游可用),
/// false 表示上游不可达,剩余日志保留、调用方应降级到本地。
pub async fn replay(
upstream: &proxy::CursorProxy,
headers: &HeaderMap,
store: &RuleStore,
) -> Result<bool> {
while let Some(entry) = store.journal_front()? {
let advanced = match entry.op {
JournalOp::Add => replay_add(upstream, headers, store, &entry.id).await?,
JournalOp::Update => replay_update(upstream, headers, store, &entry.id).await?,
JournalOp::Remove => replay_remove(upstream, headers, store, &entry.id).await?,
};
if !advanced {
return Ok(false);
}
}
Ok(true)
}
/// 用上游返回的完整列表覆盖本地镜像。仅应在日志已清空时调用。
pub fn mirror(store: &RuleStore, items: Vec<KnowledgeBaseListItem>) -> Result<()> {
let records = items
.into_iter()
.map(|item| RuleRecord {
id: item.id,
knowledge: item.knowledge,
title: item.title,
created_at: item.created_at,
is_generated: item.is_generated,
git_origin: String::new(),
})
.collect::<Vec<_>>();
store.replace_all(&records)
}
async fn replay_add(
upstream: &proxy::CursorProxy,
headers: &HeaderMap,
store: &RuleStore,
id: &str,
) -> Result<bool> {
let Some(record) = store.get(id)? else {
// 规则文件已不在(被手动删除等),日志作废。
store.pop_journal()?;
return Ok(true);
};
let message = KnowledgeBaseAddRequest {
knowledge: record.knowledge,
title: record.title,
git_origin: record.git_origin,
composer_id: None,
};
let Some(body) = send(upstream, headers, ADD_PATH, &message).await else {
return Ok(false);
};
let Ok(reply) = connect::decode_unary::<KnowledgeBaseAddResponse>(&body) else {
return Ok(false);
};
if !reply.success || reply.id.is_empty() {
tracing::warn!(
id,
"rules upstream declined replayed add; dropping journal entry"
);
store.pop_journal()?;
return Ok(true);
}
store.promote(id, &reply.id)?;
store.pop_journal()?;
tracing::info!(
local_id = id,
upstream_id = reply.id,
"replayed offline rule add to upstream"
);
Ok(true)
}
async fn replay_update(
upstream: &proxy::CursorProxy,
headers: &HeaderMap,
store: &RuleStore,
id: &str,
) -> Result<bool> {
let Some(record) = store.get(id)? else {
store.pop_journal()?;
return Ok(true);
};
let message = KnowledgeBaseUpdateRequest {
id: id.into(),
knowledge: record.knowledge,
title: record.title,
};
let Some(body) = send(upstream, headers, UPDATE_PATH, &message).await else {
return Ok(false);
};
let Ok(reply) = connect::decode_unary::<KnowledgeBaseUpdateResponse>(&body) else {
return Ok(false);
};
if !reply.success {
tracing::warn!(
id,
"rules upstream declined replayed update; dropping journal entry"
);
}
store.pop_journal()?;
Ok(true)
}
async fn replay_remove(
upstream: &proxy::CursorProxy,
headers: &HeaderMap,
store: &RuleStore,
id: &str,
) -> Result<bool> {
let message = KnowledgeBaseRemoveRequest { id: id.into() };
let Some(body) = send(upstream, headers, REMOVE_PATH, &message).await else {
return Ok(false);
};
let Ok(reply) = connect::decode_unary::<KnowledgeBaseRemoveResponse>(&body) else {
return Ok(false);
};
if !reply.success {
tracing::warn!(
id,
"rules upstream declined replayed remove; dropping journal entry"
);
}
store.pop_journal()?;
Ok(true)
}
/// 以当前请求的头为模板向上游发起一次 unary RPC。
/// 成功(2xx)返回响应体;不可达或被拒绝返回 None,由调用方保留日志。
async fn send(
upstream: &proxy::CursorProxy,
template: &HeaderMap,
path: &str,
message: &impl Message,
) -> Option<Bytes> {
let mut headers = template.clone();
// 模板里的上游 URL 头指向原始 RPC 路径,必须移除才能命中回放路径。
headers.remove(proxy::UPSTREAM_URL_HEADER);
headers.remove(header::CONTENT_LENGTH);
headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_static("application/proto"),
);
let mut request = Request::new(Body::from(message.encode_to_vec()));
*request.method_mut() = Method::POST;
*request.uri_mut() = path.parse().expect("replay path is a valid URI");
*request.headers_mut() = headers;
match proxy::forward_buffered(upstream, request).await {
Ok(response) if response.status.is_success() => Some(response.body),
Ok(response) => {
tracing::warn!(path, status = %response.status, "rules journal replay rejected by upstream");
None
}
Err(error) => {
tracing::warn!(path, %error, "rules journal replay cannot reach upstream");
None
}
}
}