fix(desktop): DeerFlow UI — ChatArea refactor + ai-elements + dead CSS cleanup
ChatArea retry button uses setInput instead of direct sendToGateway, fix bootstrap spinner stuck for non-logged-in users, remove dead CSS (aurora-title/sidebar-open/quick-action-chips), add ai components (ReasoningBlock/StreamingText/ChatMode/ModelSelector/TaskProgress), add ClassroomPlayer + ResizableChatLayout + artifact panel Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,8 +17,19 @@ pub async fn list_plans(pool: &PgPool) -> SaasResult<Vec<BillingPlan>> {
|
||||
Ok(plans)
|
||||
}
|
||||
|
||||
/// 获取单个计划
|
||||
/// 获取单个计划(公开 API 只返回 active 计划)
|
||||
pub async fn get_plan(pool: &PgPool, plan_id: &str) -> SaasResult<Option<BillingPlan>> {
|
||||
let plan = sqlx::query_as::<_, BillingPlan>(
|
||||
"SELECT * FROM billing_plans WHERE id = $1 AND status = 'active'"
|
||||
)
|
||||
.bind(plan_id)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
Ok(plan)
|
||||
}
|
||||
|
||||
/// 获取单个计划(内部使用,不过滤 status,用于已订阅用户查看旧计划)
|
||||
pub async fn get_plan_any_status(pool: &PgPool, plan_id: &str) -> SaasResult<Option<BillingPlan>> {
|
||||
let plan = sqlx::query_as::<_, BillingPlan>(
|
||||
"SELECT * FROM billing_plans WHERE id = $1"
|
||||
)
|
||||
@@ -47,7 +58,7 @@ pub async fn get_active_subscription(
|
||||
/// 获取账户当前计划(有订阅返回订阅计划,否则返回 Free)
|
||||
pub async fn get_account_plan(pool: &PgPool, account_id: &str) -> SaasResult<BillingPlan> {
|
||||
if let Some(sub) = get_active_subscription(pool, account_id).await? {
|
||||
if let Some(plan) = get_plan(pool, &sub.plan_id).await? {
|
||||
if let Some(plan) = get_plan_any_status(pool, &sub.plan_id).await? {
|
||||
return Ok(plan);
|
||||
}
|
||||
}
|
||||
@@ -81,7 +92,7 @@ pub async fn get_account_plan(pool: &PgPool, account_id: &str) -> SaasResult<Bil
|
||||
}))
|
||||
}
|
||||
|
||||
/// 获取或创建当月用量记录
|
||||
/// 获取或创建当月用量记录(原子操作,使用 INSERT ON CONFLICT 防止 TOCTOU 竞态)
|
||||
pub async fn get_or_create_usage(pool: &PgPool, account_id: &str) -> SaasResult<UsageQuota> {
|
||||
let now = chrono::Utc::now();
|
||||
let period_start = now
|
||||
@@ -91,7 +102,7 @@ pub async fn get_or_create_usage(pool: &PgPool, account_id: &str) -> SaasResult<
|
||||
.with_second(0).unwrap_or(now)
|
||||
.with_nanosecond(0).unwrap_or(now);
|
||||
|
||||
// 尝试获取现有记录
|
||||
// 先尝试获取已有记录
|
||||
let existing = sqlx::query_as::<_, UsageQuota>(
|
||||
"SELECT * FROM billing_usage_quotas \
|
||||
WHERE account_id = $1 AND period_start = $2"
|
||||
@@ -122,13 +133,15 @@ pub async fn get_or_create_usage(pool: &PgPool, account_id: &str) -> SaasResult<
|
||||
.with_second(0).unwrap_or(now)
|
||||
.with_nanosecond(0).unwrap_or(now);
|
||||
|
||||
// 使用 INSERT ON CONFLICT 原子创建(防止并发重复插入)
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let usage = sqlx::query_as::<_, UsageQuota>(
|
||||
let inserted = sqlx::query_as::<_, UsageQuota>(
|
||||
"INSERT INTO billing_usage_quotas \
|
||||
(id, account_id, period_start, period_end, \
|
||||
max_input_tokens, max_output_tokens, max_relay_requests, \
|
||||
max_hand_executions, max_pipeline_runs) \
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) \
|
||||
ON CONFLICT (account_id, period_start) DO NOTHING \
|
||||
RETURNING *"
|
||||
)
|
||||
.bind(&id)
|
||||
@@ -140,6 +153,20 @@ pub async fn get_or_create_usage(pool: &PgPool, account_id: &str) -> SaasResult<
|
||||
.bind(limits.max_relay_requests_monthly)
|
||||
.bind(limits.max_hand_executions_monthly)
|
||||
.bind(limits.max_pipeline_runs_monthly)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
if let Some(usage) = inserted {
|
||||
return Ok(usage);
|
||||
}
|
||||
|
||||
// ON CONFLICT 说明另一个并发请求已经创建了,直接查询返回
|
||||
let usage = sqlx::query_as::<_, UsageQuota>(
|
||||
"SELECT * FROM billing_usage_quotas \
|
||||
WHERE account_id = $1 AND period_start = $2"
|
||||
)
|
||||
.bind(account_id)
|
||||
.bind(period_start)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
@@ -173,7 +200,7 @@ pub async fn increment_usage(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 增加单一维度用量计数(hand_executions / pipeline_runs / relay_requests)
|
||||
/// 增加单一维度用量计数(单次 +1)
|
||||
///
|
||||
/// 使用静态 SQL 分支(白名单),避免动态列名注入风险。
|
||||
pub async fn increment_dimension(
|
||||
@@ -206,6 +233,40 @@ pub async fn increment_dimension(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 增加单一维度用量计数(批量 +N,原子操作,替代循环调用)
|
||||
///
|
||||
/// 使用静态 SQL 分支(白名单),避免动态列名注入风险。
|
||||
pub async fn increment_dimension_by(
|
||||
pool: &PgPool,
|
||||
account_id: &str,
|
||||
dimension: &str,
|
||||
count: i32,
|
||||
) -> SaasResult<()> {
|
||||
let usage = get_or_create_usage(pool, account_id).await?;
|
||||
|
||||
match dimension {
|
||||
"relay_requests" => {
|
||||
sqlx::query(
|
||||
"UPDATE billing_usage_quotas SET relay_requests = relay_requests + $1, updated_at = NOW() WHERE id = $2"
|
||||
).bind(count).bind(&usage.id).execute(pool).await?;
|
||||
}
|
||||
"hand_executions" => {
|
||||
sqlx::query(
|
||||
"UPDATE billing_usage_quotas SET hand_executions = hand_executions + $1, updated_at = NOW() WHERE id = $2"
|
||||
).bind(count).bind(&usage.id).execute(pool).await?;
|
||||
}
|
||||
"pipeline_runs" => {
|
||||
sqlx::query(
|
||||
"UPDATE billing_usage_quotas SET pipeline_runs = pipeline_runs + $1, updated_at = NOW() WHERE id = $2"
|
||||
).bind(count).bind(&usage.id).execute(pool).await?;
|
||||
}
|
||||
_ => return Err(crate::error::SaasError::InvalidInput(
|
||||
format!("Unknown usage dimension: {}", dimension)
|
||||
)),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 检查用量配额
|
||||
pub async fn check_quota(
|
||||
pool: &PgPool,
|
||||
|
||||
Reference in New Issue
Block a user