chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成
包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、 文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
173
crates/zclaw-saas/src/prompt/handlers.rs
Normal file
173
crates/zclaw-saas/src/prompt/handlers.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
//! 提示词模板 HTTP 处理器
|
||||
|
||||
use axum::{
|
||||
extract::{Extension, Path, Query, State},
|
||||
Json,
|
||||
};
|
||||
use crate::state::AppState;
|
||||
use crate::error::SaasResult;
|
||||
use crate::auth::types::AuthContext;
|
||||
use crate::auth::handlers::{log_operation, check_permission};
|
||||
use super::types::*;
|
||||
use super::service;
|
||||
|
||||
/// GET /api/v1/prompts/check — OTA 批量检查更新
|
||||
pub async fn check_updates(
|
||||
State(state): State<AppState>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
Json(req): Json<PromptCheckRequest>,
|
||||
) -> SaasResult<Json<PromptCheckResponse>> {
|
||||
let result = service::check_updates(&state.db, &req.device_id, &req.versions).await?;
|
||||
|
||||
log_operation(&state.db, &ctx.account_id, "prompt.check", "prompt", &req.device_id,
|
||||
Some(serde_json::json!({"updates_count": result.updates.len()})), ctx.client_ip.as_deref()).await?;
|
||||
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
/// GET /api/v1/prompts — 列表全部模板
|
||||
pub async fn list_prompts(
|
||||
State(state): State<AppState>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
Query(query): Query<PromptListQuery>,
|
||||
) -> SaasResult<Json<crate::common::PaginatedResponse<PromptTemplateInfo>>> {
|
||||
check_permission(&ctx, "prompt:read")?;
|
||||
Ok(Json(service::list_templates(&state.db, &query).await?))
|
||||
}
|
||||
|
||||
/// POST /api/v1/prompts — 创建提示词模板
|
||||
pub async fn create_prompt(
|
||||
State(state): State<AppState>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
Json(req): Json<CreatePromptRequest>,
|
||||
) -> SaasResult<Json<PromptTemplateInfo>> {
|
||||
check_permission(&ctx, "prompt:write")?;
|
||||
|
||||
let source = req.source.as_deref().unwrap_or("custom");
|
||||
let result = service::create_template(
|
||||
&state.db, &req.name, &req.category, req.description.as_deref(),
|
||||
source, &req.system_prompt,
|
||||
req.user_prompt_template.as_deref(),
|
||||
req.variables.clone(),
|
||||
req.min_app_version.as_deref(),
|
||||
).await?;
|
||||
|
||||
log_operation(&state.db, &ctx.account_id, "prompt.create", "prompt", &result.id,
|
||||
Some(serde_json::json!({"name": req.name})), ctx.client_ip.as_deref()).await?;
|
||||
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
/// GET /api/v1/prompts/{name} — 获取模板(按名称)
|
||||
pub async fn get_prompt(
|
||||
State(state): State<AppState>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
Path(name): Path<String>,
|
||||
) -> SaasResult<Json<PromptTemplateInfo>> {
|
||||
check_permission(&ctx, "prompt:read")?;
|
||||
Ok(Json(service::get_template_by_name(&state.db, &name).await?))
|
||||
}
|
||||
|
||||
/// PUT /api/v1/prompts/{name} — 更新模板元数据
|
||||
pub async fn update_prompt(
|
||||
State(state): State<AppState>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
Path(name): Path<String>,
|
||||
Json(req): Json<UpdatePromptRequest>,
|
||||
) -> SaasResult<Json<PromptTemplateInfo>> {
|
||||
check_permission(&ctx, "prompt:write")?;
|
||||
|
||||
let tmpl = service::get_template_by_name(&state.db, &name).await?;
|
||||
let result = service::update_template(
|
||||
&state.db, &tmpl.id,
|
||||
req.description.as_deref(),
|
||||
req.status.as_deref(),
|
||||
).await?;
|
||||
|
||||
log_operation(&state.db, &ctx.account_id, "prompt.update", "prompt", &tmpl.id,
|
||||
Some(serde_json::json!({"name": name})), ctx.client_ip.as_deref()).await?;
|
||||
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
/// DELETE /api/v1/prompts/{name} — 归档模板
|
||||
pub async fn archive_prompt(
|
||||
State(state): State<AppState>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
Path(name): Path<String>,
|
||||
) -> SaasResult<Json<PromptTemplateInfo>> {
|
||||
check_permission(&ctx, "prompt:admin")?;
|
||||
|
||||
let tmpl = service::get_template_by_name(&state.db, &name).await?;
|
||||
let result = service::update_template(&state.db, &tmpl.id, None, Some("archived")).await?;
|
||||
|
||||
log_operation(&state.db, &ctx.account_id, "prompt.archive", "prompt", &tmpl.id, None, ctx.client_ip.as_deref()).await?;
|
||||
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
/// GET /api/v1/prompts/{name}/versions — 查看版本历史
|
||||
pub async fn list_versions(
|
||||
State(state): State<AppState>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
Path(name): Path<String>,
|
||||
) -> SaasResult<Json<Vec<PromptVersionInfo>>> {
|
||||
check_permission(&ctx, "prompt:read")?;
|
||||
|
||||
let tmpl = service::get_template_by_name(&state.db, &name).await?;
|
||||
Ok(Json(service::list_versions(&state.db, &tmpl.id).await?))
|
||||
}
|
||||
|
||||
/// GET /api/v1/prompts/{name}/versions/{version} — 获取特定版本
|
||||
pub async fn get_version(
|
||||
State(state): State<AppState>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
Path((name, _version)): Path<(String, i32)>,
|
||||
) -> SaasResult<Json<PromptVersionInfo>> {
|
||||
check_permission(&ctx, "prompt:read")?;
|
||||
|
||||
let _tmpl = service::get_template_by_name(&state.db, &name).await?;
|
||||
Ok(Json(service::get_current_version(&state.db, &name).await?))
|
||||
}
|
||||
|
||||
/// POST /api/v1/prompts/{name}/versions — 发布新版本
|
||||
pub async fn create_version(
|
||||
State(state): State<AppState>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
Path(name): Path<String>,
|
||||
Json(req): Json<CreateVersionRequest>,
|
||||
) -> SaasResult<Json<PromptVersionInfo>> {
|
||||
check_permission(&ctx, "prompt:write")?;
|
||||
|
||||
let tmpl = service::get_template_by_name(&state.db, &name).await?;
|
||||
let result = service::create_version(
|
||||
&state.db, &tmpl.id,
|
||||
&req.system_prompt,
|
||||
req.user_prompt_template.as_deref(),
|
||||
req.variables.clone(),
|
||||
req.changelog.as_deref(),
|
||||
req.min_app_version.as_deref(),
|
||||
).await?;
|
||||
|
||||
log_operation(&state.db, &ctx.account_id, "prompt.publish_version", "prompt", &tmpl.id,
|
||||
Some(serde_json::json!({"name": name, "version": result.version})), ctx.client_ip.as_deref()).await?;
|
||||
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
/// POST /api/v1/prompts/{name}/rollback/{version} — 回退到指定版本
|
||||
pub async fn rollback_version(
|
||||
State(state): State<AppState>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
Path((name, version)): Path<(String, i32)>,
|
||||
) -> SaasResult<Json<PromptTemplateInfo>> {
|
||||
check_permission(&ctx, "prompt:admin")?;
|
||||
|
||||
let tmpl = service::get_template_by_name(&state.db, &name).await?;
|
||||
let result = service::rollback_version(&state.db, &tmpl.id, version).await?;
|
||||
|
||||
log_operation(&state.db, &ctx.account_id, "prompt.rollback", "prompt", &tmpl.id,
|
||||
Some(serde_json::json!({"name": name, "target_version": version})), ctx.client_ip.as_deref()).await?;
|
||||
|
||||
Ok(Json(result))
|
||||
}
|
||||
19
crates/zclaw-saas/src/prompt/mod.rs
Normal file
19
crates/zclaw-saas/src/prompt/mod.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
//! 提示词模板管理模块
|
||||
|
||||
pub mod types;
|
||||
pub mod service;
|
||||
pub mod handlers;
|
||||
|
||||
use axum::routing::{get, post};
|
||||
use crate::state::AppState;
|
||||
|
||||
/// 提示词管理路由 (需要认证)
|
||||
pub fn routes() -> axum::Router<AppState> {
|
||||
axum::Router::new()
|
||||
.route("/api/v1/prompts/check", post(handlers::check_updates))
|
||||
.route("/api/v1/prompts", get(handlers::list_prompts).post(handlers::create_prompt))
|
||||
.route("/api/v1/prompts/:name", get(handlers::get_prompt).put(handlers::update_prompt).delete(handlers::archive_prompt))
|
||||
.route("/api/v1/prompts/:name/versions", get(handlers::list_versions).post(handlers::create_version))
|
||||
.route("/api/v1/prompts/:name/versions/:version", get(handlers::get_version))
|
||||
.route("/api/v1/prompts/:name/rollback/:version", post(handlers::rollback_version))
|
||||
}
|
||||
323
crates/zclaw-saas/src/prompt/service.rs
Normal file
323
crates/zclaw-saas/src/prompt/service.rs
Normal file
@@ -0,0 +1,323 @@
|
||||
//! 提示词模板服务层
|
||||
|
||||
use sqlx::PgPool;
|
||||
use crate::error::{SaasError, SaasResult};
|
||||
use crate::common::PaginatedResponse;
|
||||
use crate::common::normalize_pagination;
|
||||
use super::types::*;
|
||||
|
||||
/// 创建提示词模板 + 初始版本
|
||||
pub async fn create_template(
|
||||
db: &PgPool,
|
||||
name: &str,
|
||||
category: &str,
|
||||
description: Option<&str>,
|
||||
source: &str,
|
||||
system_prompt: &str,
|
||||
user_prompt_template: Option<&str>,
|
||||
variables: Option<serde_json::Value>,
|
||||
min_app_version: Option<&str>,
|
||||
) -> SaasResult<PromptTemplateInfo> {
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let version_id = uuid::Uuid::new_v4().to_string();
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let vars_json = variables.unwrap_or(serde_json::json!([])).to_string();
|
||||
|
||||
// 插入模板
|
||||
sqlx::query(
|
||||
"INSERT INTO prompt_templates (id, name, category, description, source, current_version, status, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, 1, 'active', $6, $6)"
|
||||
)
|
||||
.bind(&id).bind(name).bind(category).bind(description).bind(source).bind(&now)
|
||||
.execute(db).await.map_err(|e| {
|
||||
if e.to_string().contains("unique") {
|
||||
SaasError::AlreadyExists(format!("提示词模板 '{}' 已存在", name))
|
||||
} else {
|
||||
SaasError::Database(e)
|
||||
}
|
||||
})?;
|
||||
|
||||
// 插入 v1
|
||||
sqlx::query(
|
||||
"INSERT INTO prompt_versions (id, template_id, version, system_prompt, user_prompt_template, variables, min_app_version, created_at)
|
||||
VALUES ($1, $2, 1, $3, $4, $5, $6, $7)"
|
||||
)
|
||||
.bind(&version_id).bind(&id).bind(system_prompt).bind(user_prompt_template).bind(&vars_json).bind(min_app_version).bind(&now)
|
||||
.execute(db).await?;
|
||||
|
||||
get_template(db, &id).await
|
||||
}
|
||||
|
||||
/// 获取单个模板
|
||||
pub async fn get_template(db: &PgPool, id: &str) -> SaasResult<PromptTemplateInfo> {
|
||||
let row: Option<(String, String, String, Option<String>, String, i32, String, String, String)> =
|
||||
sqlx::query_as(
|
||||
"SELECT id, name, category, description, source, current_version, status, created_at, updated_at
|
||||
FROM prompt_templates WHERE id = $1"
|
||||
).bind(id).fetch_optional(db).await?;
|
||||
|
||||
let (id, name, category, description, source, current_version, status, created_at, updated_at) =
|
||||
row.ok_or_else(|| SaasError::NotFound(format!("提示词模板 {} 不存在", id)))?;
|
||||
|
||||
Ok(PromptTemplateInfo { id, name, category, description, source, current_version, status, created_at, updated_at })
|
||||
}
|
||||
|
||||
/// 按名称获取模板
|
||||
pub async fn get_template_by_name(db: &PgPool, name: &str) -> SaasResult<PromptTemplateInfo> {
|
||||
let row: Option<(String, String, String, Option<String>, String, i32, String, String, String)> =
|
||||
sqlx::query_as(
|
||||
"SELECT id, name, category, description, source, current_version, status, created_at, updated_at
|
||||
FROM prompt_templates WHERE name = $1"
|
||||
).bind(name).fetch_optional(db).await?;
|
||||
|
||||
let (id, name, category, description, source, current_version, status, created_at, updated_at) =
|
||||
row.ok_or_else(|| SaasError::NotFound(format!("提示词模板 '{}' 不存在", name)))?;
|
||||
|
||||
Ok(PromptTemplateInfo { id, name, category, description, source, current_version, status, created_at, updated_at })
|
||||
}
|
||||
|
||||
/// 列表模板
|
||||
pub async fn list_templates(
|
||||
db: &PgPool,
|
||||
query: &PromptListQuery,
|
||||
) -> SaasResult<PaginatedResponse<PromptTemplateInfo>> {
|
||||
let (page, page_size, offset) = normalize_pagination(query.page, query.page_size);
|
||||
|
||||
let mut where_clauses = vec!["1=1".to_string()];
|
||||
let mut count_sql = String::from("SELECT COUNT(*) FROM prompt_templates WHERE ");
|
||||
let mut data_sql = String::from(
|
||||
"SELECT id, name, category, description, source, current_version, status, created_at, updated_at
|
||||
FROM prompt_templates WHERE "
|
||||
);
|
||||
|
||||
if let Some(ref cat) = query.category {
|
||||
where_clauses.push(format!("category = '{}'", cat.replace('\'', "''")));
|
||||
}
|
||||
if let Some(ref src) = query.source {
|
||||
where_clauses.push(format!("source = '{}'", src.replace('\'', "''")));
|
||||
}
|
||||
if let Some(ref st) = query.status {
|
||||
where_clauses.push(format!("status = '{}'", st.replace('\'', "''")));
|
||||
}
|
||||
|
||||
let where_clause = where_clauses.join(" AND ");
|
||||
count_sql.push_str(&where_clause);
|
||||
data_sql.push_str(&where_clause);
|
||||
data_sql.push_str(&format!(" ORDER BY updated_at DESC LIMIT {} OFFSET {}", page_size, offset));
|
||||
|
||||
let total: i64 = sqlx::query_scalar(&count_sql).fetch_one(db).await?;
|
||||
|
||||
let rows: Vec<(String, String, String, Option<String>, String, i32, String, String, String)> =
|
||||
sqlx::query_as(&data_sql).fetch_all(db).await?;
|
||||
|
||||
let items: Vec<PromptTemplateInfo> = rows.into_iter().map(|(id, name, category, description, source, current_version, status, created_at, updated_at)| {
|
||||
PromptTemplateInfo { id, name, category, description, source, current_version, status, created_at, updated_at }
|
||||
}).collect();
|
||||
|
||||
Ok(PaginatedResponse { items, total, page, page_size })
|
||||
}
|
||||
|
||||
/// 更新模板元数据(不修改内容)
|
||||
pub async fn update_template(
|
||||
db: &PgPool,
|
||||
id: &str,
|
||||
description: Option<&str>,
|
||||
status: Option<&str>,
|
||||
) -> SaasResult<PromptTemplateInfo> {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
if let Some(desc) = description {
|
||||
sqlx::query("UPDATE prompt_templates SET description = $1, updated_at = $2 WHERE id = $3")
|
||||
.bind(desc).bind(&now).bind(id).execute(db).await?;
|
||||
}
|
||||
if let Some(st) = status {
|
||||
let valid = ["active", "deprecated", "archived"];
|
||||
if !valid.contains(&st) {
|
||||
return Err(SaasError::InvalidInput(format!("无效状态: {},允许: {}", st, valid.join(", "))));
|
||||
}
|
||||
sqlx::query("UPDATE prompt_templates SET status = $1, updated_at = $2 WHERE id = $3")
|
||||
.bind(st).bind(&now).bind(id).execute(db).await?;
|
||||
}
|
||||
|
||||
get_template(db, id).await
|
||||
}
|
||||
|
||||
/// 发布新版本
|
||||
pub async fn create_version(
|
||||
db: &PgPool,
|
||||
template_id: &str,
|
||||
system_prompt: &str,
|
||||
user_prompt_template: Option<&str>,
|
||||
variables: Option<serde_json::Value>,
|
||||
changelog: Option<&str>,
|
||||
min_app_version: Option<&str>,
|
||||
) -> SaasResult<PromptVersionInfo> {
|
||||
let tmpl = get_template(db, template_id).await?;
|
||||
|
||||
let new_version = tmpl.current_version + 1;
|
||||
let version_id = uuid::Uuid::new_v4().to_string();
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let vars_json = variables.unwrap_or(serde_json::json!([])).to_string();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO prompt_versions (id, template_id, version, system_prompt, user_prompt_template, variables, changelog, min_app_version, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)"
|
||||
)
|
||||
.bind(&version_id).bind(template_id).bind(new_version)
|
||||
.bind(system_prompt).bind(user_prompt_template).bind(&vars_json).bind(changelog).bind(min_app_version).bind(&now)
|
||||
.execute(db).await?;
|
||||
|
||||
// 更新模板的 current_version
|
||||
sqlx::query("UPDATE prompt_templates SET current_version = $1, updated_at = $2 WHERE id = $3")
|
||||
.bind(new_version).bind(&now).bind(template_id)
|
||||
.execute(db).await?;
|
||||
|
||||
get_version(db, &version_id).await
|
||||
}
|
||||
|
||||
/// 获取特定版本
|
||||
pub async fn get_version(db: &PgPool, version_id: &str) -> SaasResult<PromptVersionInfo> {
|
||||
let row: Option<(String, String, i32, String, Option<String>, String, Option<String>, Option<String>, String)> =
|
||||
sqlx::query_as(
|
||||
"SELECT id, template_id, version, system_prompt, user_prompt_template, variables, changelog, min_app_version, created_at
|
||||
FROM prompt_versions WHERE id = $1"
|
||||
).bind(version_id).fetch_optional(db).await?;
|
||||
|
||||
let (id, template_id, version, system_prompt, user_prompt_template, variables_str, changelog, min_app_version, created_at) =
|
||||
row.ok_or_else(|| SaasError::NotFound(format!("提示词版本 {} 不存在", version_id)))?;
|
||||
|
||||
let variables: serde_json::Value = serde_json::from_str(&variables_str).unwrap_or(serde_json::json!([]));
|
||||
|
||||
Ok(PromptVersionInfo { id, template_id, version, system_prompt, user_prompt_template, variables, changelog, min_app_version, created_at })
|
||||
}
|
||||
|
||||
/// 获取模板的当前版本内容
|
||||
pub async fn get_current_version(db: &PgPool, template_name: &str) -> SaasResult<PromptVersionInfo> {
|
||||
let tmpl = get_template_by_name(db, template_name).await?;
|
||||
|
||||
let row: Option<(String, String, i32, String, Option<String>, String, Option<String>, Option<String>, String)> =
|
||||
sqlx::query_as(
|
||||
"SELECT id, template_id, version, system_prompt, user_prompt_template, variables, changelog, min_app_version, created_at
|
||||
FROM prompt_versions WHERE template_id = $1 AND version = $2"
|
||||
).bind(&tmpl.id).bind(tmpl.current_version).fetch_optional(db).await?;
|
||||
|
||||
let (id, template_id, version, system_prompt, user_prompt_template, variables_str, changelog, min_app_version, created_at) =
|
||||
row.ok_or_else(|| SaasError::NotFound(format!("提示词 '{}' 的版本 {} 不存在", template_name, tmpl.current_version)))?;
|
||||
|
||||
let variables: serde_json::Value = serde_json::from_str(&variables_str).unwrap_or(serde_json::json!([]));
|
||||
|
||||
Ok(PromptVersionInfo { id, template_id, version, system_prompt, user_prompt_template, variables, changelog, min_app_version, created_at })
|
||||
}
|
||||
|
||||
/// 列出模板的所有版本
|
||||
pub async fn list_versions(
|
||||
db: &PgPool,
|
||||
template_id: &str,
|
||||
) -> SaasResult<Vec<PromptVersionInfo>> {
|
||||
let rows: Vec<(String, String, i32, String, Option<String>, String, Option<String>, Option<String>, String)> =
|
||||
sqlx::query_as(
|
||||
"SELECT id, template_id, version, system_prompt, user_prompt_template, variables, changelog, min_app_version, created_at
|
||||
FROM prompt_versions WHERE template_id = $1 ORDER BY version DESC"
|
||||
).bind(template_id).fetch_all(db).await?;
|
||||
|
||||
Ok(rows.into_iter().map(|(id, template_id, version, system_prompt, user_prompt_template, variables_str, changelog, min_app_version, created_at)| {
|
||||
let variables = serde_json::from_str(&variables_str).unwrap_or(serde_json::json!([]));
|
||||
PromptVersionInfo { id, template_id, version, system_prompt, user_prompt_template, variables, changelog, min_app_version, created_at }
|
||||
}).collect())
|
||||
}
|
||||
|
||||
/// 回退到指定版本
|
||||
pub async fn rollback_version(
|
||||
db: &PgPool,
|
||||
template_id: &str,
|
||||
target_version: i32,
|
||||
) -> SaasResult<PromptTemplateInfo> {
|
||||
// 验证目标版本存在
|
||||
let exists: (bool,) = sqlx::query_as(
|
||||
"SELECT EXISTS(SELECT 1 FROM prompt_versions WHERE template_id = $1 AND version = $2)"
|
||||
).bind(template_id).bind(target_version).fetch_one(db).await?;
|
||||
|
||||
if !exists.0 {
|
||||
return Err(SaasError::NotFound(format!("版本 {} 不存在", target_version)));
|
||||
}
|
||||
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
sqlx::query("UPDATE prompt_templates SET current_version = $1, updated_at = $2 WHERE id = $3")
|
||||
.bind(target_version).bind(&now).bind(template_id)
|
||||
.execute(db).await?;
|
||||
|
||||
get_template(db, template_id).await
|
||||
}
|
||||
|
||||
/// OTA 批量检查更新
|
||||
pub async fn check_updates(
|
||||
db: &PgPool,
|
||||
device_id: &str,
|
||||
client_versions: &std::collections::HashMap<String, i32>,
|
||||
) -> SaasResult<PromptCheckResponse> {
|
||||
let mut updates = Vec::new();
|
||||
|
||||
for (name, client_ver) in client_versions {
|
||||
let tmpl = match get_template_by_name(db, name).await {
|
||||
Ok(t) if t.status == "active" => t,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
if tmpl.current_version > *client_ver {
|
||||
// 获取最新版本内容
|
||||
if let Ok(ver) = get_current_version(db, name).await {
|
||||
updates.push(PromptUpdatePayload {
|
||||
name: tmpl.name.clone(),
|
||||
version: ver.version,
|
||||
system_prompt: ver.system_prompt,
|
||||
user_prompt_template: ver.user_prompt_template,
|
||||
variables: ver.variables,
|
||||
source: tmpl.source.clone(),
|
||||
min_app_version: ver.min_app_version,
|
||||
changelog: ver.changelog,
|
||||
});
|
||||
|
||||
// 更新同步状态
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
sqlx::query(
|
||||
"INSERT INTO prompt_sync_status (device_id, template_id, synced_version, synced_at)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (device_id, template_id) DO UPDATE SET synced_version = $3, synced_at = $4"
|
||||
)
|
||||
.bind(device_id).bind(&tmpl.id).bind(ver.version).bind(&now)
|
||||
.execute(db).await.ok(); // 非关键,忽略错误
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 首次连接:返回所有 active 的 builtin 模板
|
||||
if client_versions.is_empty() {
|
||||
let all_templates = list_templates(db, &PromptListQuery {
|
||||
source: Some("builtin".into()),
|
||||
status: Some("active".into()),
|
||||
category: None,
|
||||
page: None,
|
||||
page_size: Some(100),
|
||||
}).await?;
|
||||
|
||||
for tmpl in &all_templates.items {
|
||||
if let Ok(ver) = get_current_version(db, &tmpl.name).await {
|
||||
updates.push(PromptUpdatePayload {
|
||||
name: tmpl.name.clone(),
|
||||
version: ver.version,
|
||||
system_prompt: ver.system_prompt,
|
||||
user_prompt_template: ver.user_prompt_template,
|
||||
variables: ver.variables,
|
||||
source: tmpl.source.clone(),
|
||||
min_app_version: ver.min_app_version,
|
||||
changelog: ver.changelog,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(PromptCheckResponse {
|
||||
updates,
|
||||
server_time: chrono::Utc::now().to_rfc3339(),
|
||||
})
|
||||
}
|
||||
97
crates/zclaw-saas/src/prompt/types.rs
Normal file
97
crates/zclaw-saas/src/prompt/types.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
//! 提示词模板类型定义
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// --- Prompt Template ---
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PromptTemplateInfo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub category: String,
|
||||
pub description: Option<String>,
|
||||
pub source: String,
|
||||
pub current_version: i32,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreatePromptRequest {
|
||||
pub name: String,
|
||||
pub category: String,
|
||||
pub description: Option<String>,
|
||||
pub source: Option<String>,
|
||||
pub system_prompt: String,
|
||||
pub user_prompt_template: Option<String>,
|
||||
pub variables: Option<serde_json::Value>,
|
||||
pub min_app_version: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdatePromptRequest {
|
||||
pub description: Option<String>,
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
// --- Prompt Version ---
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PromptVersionInfo {
|
||||
pub id: String,
|
||||
pub template_id: String,
|
||||
pub version: i32,
|
||||
pub system_prompt: String,
|
||||
pub user_prompt_template: Option<String>,
|
||||
pub variables: serde_json::Value,
|
||||
pub changelog: Option<String>,
|
||||
pub min_app_version: Option<String>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateVersionRequest {
|
||||
pub system_prompt: String,
|
||||
pub user_prompt_template: Option<String>,
|
||||
pub variables: Option<serde_json::Value>,
|
||||
pub changelog: Option<String>,
|
||||
pub min_app_version: Option<String>,
|
||||
}
|
||||
|
||||
// --- OTA Check ---
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PromptCheckRequest {
|
||||
pub device_id: String,
|
||||
pub versions: std::collections::HashMap<String, i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PromptCheckResponse {
|
||||
pub updates: Vec<PromptUpdatePayload>,
|
||||
pub server_time: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct PromptUpdatePayload {
|
||||
pub name: String,
|
||||
pub version: i32,
|
||||
pub system_prompt: String,
|
||||
pub user_prompt_template: Option<String>,
|
||||
pub variables: serde_json::Value,
|
||||
pub source: String,
|
||||
pub min_app_version: Option<String>,
|
||||
pub changelog: Option<String>,
|
||||
}
|
||||
|
||||
// --- List ---
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PromptListQuery {
|
||||
pub category: Option<String>,
|
||||
pub source: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub page: Option<u32>,
|
||||
pub page_size: Option<u32>,
|
||||
}
|
||||
Reference in New Issue
Block a user