chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成
包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、 文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
@@ -49,8 +49,26 @@ CREATE TABLE IF NOT EXISTS schema_version (
|
||||
version INTEGER PRIMARY KEY
|
||||
);
|
||||
|
||||
-- Hand execution runs table
|
||||
CREATE TABLE IF NOT EXISTS hand_runs (
|
||||
id TEXT PRIMARY KEY,
|
||||
hand_name TEXT NOT NULL,
|
||||
trigger_source TEXT NOT NULL,
|
||||
params TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
result TEXT,
|
||||
error TEXT,
|
||||
duration_ms INTEGER,
|
||||
created_at TEXT NOT NULL,
|
||||
started_at TEXT,
|
||||
completed_at TEXT
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_kv_agent ON kv_store(agent_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_hand_runs_hand ON hand_runs(hand_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_hand_runs_status ON hand_runs(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_hand_runs_created ON hand_runs(created_at);
|
||||
"#;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Memory store implementation
|
||||
|
||||
use sqlx::SqlitePool;
|
||||
use zclaw_types::{AgentConfig, AgentId, SessionId, Message, Result, ZclawError};
|
||||
use zclaw_types::{AgentConfig, AgentId, SessionId, Message, Result, ZclawError, HandRun, HandRunId, HandRunStatus, HandRunFilter};
|
||||
|
||||
/// Memory store for persisting ZCLAW data
|
||||
pub struct MemoryStore {
|
||||
@@ -283,6 +283,193 @@ impl MemoryStore {
|
||||
|
||||
Ok(rows.into_iter().map(|(key,)| key).collect())
|
||||
}
|
||||
|
||||
// === Hand Run Tracking ===
|
||||
|
||||
/// Save a new hand run record
|
||||
pub async fn save_hand_run(&self, run: &HandRun) -> Result<()> {
|
||||
let id = run.id.to_string();
|
||||
let trigger_source = serde_json::to_string(&run.trigger_source)?;
|
||||
let params = serde_json::to_string(&run.params)?;
|
||||
let result = run.result.as_ref().map(|v| serde_json::to_string(v)).transpose()?;
|
||||
let error = run.error.as_ref().map(|e| serde_json::to_string(e)).transpose()?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO hand_runs (id, hand_name, trigger_source, params, status, result, error, duration_ms, created_at, started_at, completed_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
)
|
||||
.bind(&id)
|
||||
.bind(&run.hand_name)
|
||||
.bind(&trigger_source)
|
||||
.bind(¶ms)
|
||||
.bind(run.status.to_string())
|
||||
.bind(result.as_deref())
|
||||
.bind(error.as_deref())
|
||||
.bind(run.duration_ms.map(|d| d as i64))
|
||||
.bind(&run.created_at)
|
||||
.bind(run.started_at.as_deref())
|
||||
.bind(run.completed_at.as_deref())
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(|e| ZclawError::StorageError(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update an existing hand run record
|
||||
pub async fn update_hand_run(&self, run: &HandRun) -> Result<()> {
|
||||
let id = run.id.to_string();
|
||||
let trigger_source = serde_json::to_string(&run.trigger_source)?;
|
||||
let params = serde_json::to_string(&run.params)?;
|
||||
let result = run.result.as_ref().map(|v| serde_json::to_string(v)).transpose()?;
|
||||
let error = run.error.as_ref().map(|e| serde_json::to_string(e)).transpose()?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE hand_runs SET
|
||||
hand_name = ?, trigger_source = ?, params = ?, status = ?,
|
||||
result = ?, error = ?, duration_ms = ?,
|
||||
started_at = ?, completed_at = ?
|
||||
WHERE id = ?
|
||||
"#,
|
||||
)
|
||||
.bind(&run.hand_name)
|
||||
.bind(&trigger_source)
|
||||
.bind(¶ms)
|
||||
.bind(run.status.to_string())
|
||||
.bind(result.as_deref())
|
||||
.bind(error.as_deref())
|
||||
.bind(run.duration_ms.map(|d| d as i64))
|
||||
.bind(run.started_at.as_deref())
|
||||
.bind(run.completed_at.as_deref())
|
||||
.bind(&id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(|e| ZclawError::StorageError(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a hand run by ID
|
||||
pub async fn get_hand_run(&self, id: &HandRunId) -> Result<Option<HandRun>> {
|
||||
let id_str = id.to_string();
|
||||
|
||||
let row = sqlx::query_as::<_, (String, String, String, String, String, Option<String>, Option<String>, Option<i64>, String, Option<String>, Option<String>)>(
|
||||
"SELECT id, hand_name, trigger_source, params, status, result, error, duration_ms, created_at, started_at, completed_at FROM hand_runs WHERE id = ?"
|
||||
)
|
||||
.bind(&id_str)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(|e| ZclawError::StorageError(e.to_string()))?;
|
||||
|
||||
match row {
|
||||
Some(r) => Ok(Some(Self::row_to_hand_run(r)?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// List hand runs with optional filter
|
||||
pub async fn list_hand_runs(&self, filter: &HandRunFilter) -> Result<Vec<HandRun>> {
|
||||
let mut query = String::from(
|
||||
"SELECT id, hand_name, trigger_source, params, status, result, error, duration_ms, created_at, started_at, completed_at FROM hand_runs WHERE 1=1"
|
||||
);
|
||||
let mut bind_values: Vec<String> = Vec::new();
|
||||
|
||||
if let Some(ref hand_name) = filter.hand_name {
|
||||
query.push_str(" AND hand_name = ?");
|
||||
bind_values.push(hand_name.clone());
|
||||
}
|
||||
|
||||
if let Some(ref status) = filter.status {
|
||||
query.push_str(" AND status = ?");
|
||||
bind_values.push(status.to_string());
|
||||
}
|
||||
|
||||
query.push_str(" ORDER BY created_at DESC");
|
||||
|
||||
if let Some(limit) = filter.limit {
|
||||
query.push_str(&format!(" LIMIT {}", limit));
|
||||
}
|
||||
if let Some(offset) = filter.offset {
|
||||
query.push_str(&format!(" OFFSET {}", offset));
|
||||
}
|
||||
|
||||
let mut sql_query = sqlx::query_as::<_, (String, String, String, String, String, Option<String>, Option<String>, Option<i64>, String, Option<String>, Option<String>)>(&query);
|
||||
|
||||
for val in &bind_values {
|
||||
sql_query = sql_query.bind(val);
|
||||
}
|
||||
|
||||
let rows = sql_query
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(|e| ZclawError::StorageError(e.to_string()))?;
|
||||
|
||||
rows.into_iter()
|
||||
.map(|r| Self::row_to_hand_run(r))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Count hand runs matching filter
|
||||
pub async fn count_hand_runs(&self, filter: &HandRunFilter) -> Result<u32> {
|
||||
let mut query = String::from("SELECT COUNT(*) FROM hand_runs WHERE 1=1");
|
||||
let mut bind_values: Vec<String> = Vec::new();
|
||||
|
||||
if let Some(ref hand_name) = filter.hand_name {
|
||||
query.push_str(" AND hand_name = ?");
|
||||
bind_values.push(hand_name.clone());
|
||||
}
|
||||
if let Some(ref status) = filter.status {
|
||||
query.push_str(" AND status = ?");
|
||||
bind_values.push(status.to_string());
|
||||
}
|
||||
|
||||
let mut sql_query = sqlx::query_scalar::<_, i64>(&query);
|
||||
for val in &bind_values {
|
||||
sql_query = sql_query.bind(val);
|
||||
}
|
||||
|
||||
let count = sql_query
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(|e| ZclawError::StorageError(e.to_string()))?;
|
||||
|
||||
Ok(count as u32)
|
||||
}
|
||||
|
||||
fn row_to_hand_run(
|
||||
row: (String, String, String, String, String, Option<String>, Option<String>, Option<i64>, String, Option<String>, Option<String>),
|
||||
) -> Result<HandRun> {
|
||||
let (id, hand_name, trigger_source, params, status, result, error, duration_ms, created_at, started_at, completed_at) = row;
|
||||
|
||||
let run_id: HandRunId = id.parse()
|
||||
.map_err(|e| ZclawError::StorageError(format!("Invalid HandRunId: {}", e)))?;
|
||||
let trigger: zclaw_types::TriggerSource = serde_json::from_str(&trigger_source)?;
|
||||
let params_val: serde_json::Value = serde_json::from_str(¶ms)?;
|
||||
let run_status: HandRunStatus = status.parse()
|
||||
.map_err(|e| ZclawError::StorageError(e))?;
|
||||
let result_val: Option<serde_json::Value> = result.map(|r| serde_json::from_str(&r)).transpose()?;
|
||||
let error_val: Option<String> = error.as_ref()
|
||||
.map(|e| serde_json::from_str::<String>(e))
|
||||
.transpose()
|
||||
.unwrap_or_else(|_| error.clone());
|
||||
|
||||
Ok(HandRun {
|
||||
id: run_id,
|
||||
hand_name,
|
||||
trigger_source: trigger,
|
||||
params: params_val,
|
||||
status: run_status,
|
||||
result: result_val,
|
||||
error: error_val,
|
||||
duration_ms: duration_ms.map(|d| d as u64),
|
||||
created_at,
|
||||
started_at,
|
||||
completed_at,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user