chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成

包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、
文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
iven
2026-03-29 10:46:26 +08:00
parent 9a5fad2b59
commit 5fdf96c3f5
268 changed files with 22011 additions and 3886 deletions

View File

@@ -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);
"#;

View File

@@ -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(&params)
.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(&params)
.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(&params)?;
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)]