fix(desktop): session persistence — refresh/login/context/empty-content 4-bug fix
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
1. App.tsx: add restoreSession() call on startup to prevent redirect to login page after refresh (isRestoring guard + BootstrapScreen) 2. CloneManager: call syncAgents() after loadClones() to restore currentAgent and conversation history on app load 3. zclaw-memory: add get_or_create_session() so frontend session UUID is persisted directly — kernel no longer creates mismatched IDs 4. openai.rs: assistant message content must be non-empty for Kimi/Qwen APIs — replace empty content with meaningful placeholders Also includes admin-v2 ModelServices unified page (merge providers + models + API keys into expandable row layout)
This commit is contained in:
@@ -645,15 +645,11 @@ impl Kernel {
|
||||
// Reuse existing session or create new one
|
||||
let session_id = match session_id_override {
|
||||
Some(id) => {
|
||||
// Verify the session exists; if not, create a new one
|
||||
let existing = self.memory.get_messages(&id).await;
|
||||
match existing {
|
||||
Ok(msgs) if !msgs.is_empty() => id,
|
||||
_ => {
|
||||
tracing::debug!("Session {} not found or empty, creating new session", id);
|
||||
self.memory.create_session(agent_id).await?
|
||||
}
|
||||
}
|
||||
// Use get_or_create to ensure the frontend's session ID is persisted.
|
||||
// This is the critical bridge: without it, the kernel generates a
|
||||
// different UUID each turn, so conversation history is never found.
|
||||
tracing::debug!("Reusing frontend session ID: {}", id);
|
||||
self.memory.get_or_create_session(&id, agent_id).await?
|
||||
}
|
||||
None => self.memory.create_session(agent_id).await?,
|
||||
};
|
||||
|
||||
@@ -173,6 +173,49 @@ impl MemoryStore {
|
||||
Ok(session_id)
|
||||
}
|
||||
|
||||
/// Get an existing session or create it with the given ID.
|
||||
///
|
||||
/// This is the critical bridge between frontend session IDs and the database.
|
||||
/// The frontend generates a UUID (`sessionKey`) and sends it with each message.
|
||||
/// Without this method, the kernel would create a *different* session ID on
|
||||
/// every call, so conversation history would never be found.
|
||||
pub async fn get_or_create_session(
|
||||
&self,
|
||||
session_id: &SessionId,
|
||||
agent_id: &AgentId,
|
||||
) -> Result<SessionId> {
|
||||
let session_str = session_id.to_string();
|
||||
let agent_str = agent_id.to_string();
|
||||
|
||||
// Check if session already exists
|
||||
let exists: bool = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) > 0 FROM sessions WHERE id = ?",
|
||||
)
|
||||
.bind(&session_str)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
|
||||
if exists {
|
||||
return Ok(session_id.clone());
|
||||
}
|
||||
|
||||
// Create session with the frontend-provided ID
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO sessions (id, agent_id, created_at, updated_at)
|
||||
VALUES (?, ?, datetime('now'), datetime('now'))
|
||||
"#,
|
||||
)
|
||||
.bind(&session_str)
|
||||
.bind(&agent_str)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(|e| ZclawError::StorageError(e.to_string()))?;
|
||||
|
||||
Ok(session_id.clone())
|
||||
}
|
||||
|
||||
/// Append a message to a session
|
||||
pub async fn append_message(&self, session_id: &SessionId, message: &Message) -> Result<()> {
|
||||
let session_str = session_id.to_string();
|
||||
|
||||
@@ -360,10 +360,14 @@ impl OpenAiDriver {
|
||||
|
||||
if let Some(calls) = calls {
|
||||
if !calls.is_empty() {
|
||||
// Merge assistant content + reasoning into the tool call message
|
||||
// Merge assistant content + reasoning into the tool call message.
|
||||
// IMPORTANT: Some APIs (Kimi, Qwen) require `content` to be non-empty
|
||||
// even when tool_calls is set. Use a meaningful placeholder if content is empty.
|
||||
let content_value = content.filter(|s| !s.trim().is_empty())
|
||||
.unwrap_or_else(|| "正在调用工具...".to_string());
|
||||
out.push(OpenAiMessage {
|
||||
role: "assistant".to_string(),
|
||||
content: content.filter(|s| !s.is_empty()),
|
||||
content: Some(content_value),
|
||||
reasoning_content: reasoning.filter(|s| !s.is_empty()),
|
||||
tool_calls: Some(calls),
|
||||
tool_call_id: None,
|
||||
@@ -371,11 +375,14 @@ impl OpenAiDriver {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// No tool calls — emit a plain assistant message
|
||||
// No tool calls — emit a plain assistant message.
|
||||
// Ensure content is always Some() and non-empty to satisfy API requirements.
|
||||
if content.is_some() || reasoning.is_some() {
|
||||
let content_value = content.filter(|s| !s.trim().is_empty())
|
||||
.unwrap_or_else(|| "正在思考...".to_string());
|
||||
out.push(OpenAiMessage {
|
||||
role: "assistant".to_string(),
|
||||
content: content.filter(|s| !s.is_empty()),
|
||||
content: Some(content_value),
|
||||
reasoning_content: reasoning.filter(|s| !s.is_empty()),
|
||||
tool_calls: None,
|
||||
tool_call_id: None,
|
||||
|
||||
Reference in New Issue
Block a user