feat: thinking_content column + first-project budget logic
- Add thinking_content column to room_message table - Migration for thinking_content column - ws-protocol update with streaming chunk types - Billing: first project gets $10, first workspace gets $30 - Subsequent projects/workspaces get $0 budget
This commit is contained in:
parent
0939aa240b
commit
07e74c230c
@ -60,7 +60,7 @@ pub async fn admin_workspace_add_credit(
|
||||
}
|
||||
|
||||
let ws = service.utils_find_workspace_by_slug(slug.clone()).await?;
|
||||
let billing = service.ensure_workspace_billing(ws.id).await?;
|
||||
let billing = service.ensure_workspace_billing(ws.id, None).await?;
|
||||
let now_utc = Utc::now();
|
||||
|
||||
let new_balance = rust_decimal::Decimal::from_f64_retain(
|
||||
|
||||
@ -2,6 +2,7 @@ pub use sea_orm_migration::prelude::*;
|
||||
|
||||
mod m20260420_000003_add_model_id_to_room_message;
|
||||
pub mod m20260421_000001_add_agent_type_to_room_ai;
|
||||
pub mod m20260426_000001_add_thinking_content_to_room_message;
|
||||
|
||||
pub async fn execute_sql(manager: &SchemaManager<'_>, sql: &str) -> Result<(), DbErr> {
|
||||
for stmt in split_sql_statements(sql) {
|
||||
@ -89,7 +90,7 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20260420_000002_add_push_subscription::Migration),
|
||||
Box::new(m20260420_000003_add_model_id_to_room_message::Migration),
|
||||
Box::new(m20260421_000001_add_agent_type_to_room_ai::Migration),
|
||||
Box::new(m20260420_000003_add_model_id_to_room_message::Migration),
|
||||
Box::new(m20260426_000001_add_thinking_content_to_room_message::Migration),
|
||||
// Repo tables
|
||||
Box::new(m20250628_000028_create_repo::Migration),
|
||||
Box::new(m20250628_000029_create_repo_branch::Migration),
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
//! SeaORM migration: add thinking_content column to room_message
|
||||
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
impl MigrationName for Migration {
|
||||
fn name(&self) -> &str {
|
||||
"m20260426_000001_add_thinking_content_to_room_message"
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let sql = include_str!("sql/m20260426_000001_add_thinking_content_to_room_message.sql");
|
||||
super::execute_sql(manager, sql).await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.get_connection()
|
||||
.execute_raw(sea_orm::Statement::from_string(
|
||||
sea_orm::DbBackend::Postgres,
|
||||
"ALTER TABLE room_message DROP COLUMN IF EXISTS thinking_content;",
|
||||
))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
ALTER TABLE room_message ADD COLUMN IF NOT EXISTS thinking_content TEXT;
|
||||
@ -19,6 +19,8 @@ pub struct Model {
|
||||
pub in_reply_to: Option<MessageId>,
|
||||
pub content: String,
|
||||
pub content_type: MessageContentType,
|
||||
/// Accumulated AI reasoning/thinking text.
|
||||
pub thinking_content: Option<String>,
|
||||
pub edited_at: Option<DateTimeUtc>,
|
||||
pub send_at: DateTimeUtc,
|
||||
pub revoked: Option<DateTimeUtc>,
|
||||
|
||||
@ -149,9 +149,24 @@ impl AppService {
|
||||
}
|
||||
|
||||
let now_utc = Utc::now();
|
||||
// Only first project per user gets initial budget ($10)
|
||||
let initial_balance = if let Some(uid) = user_uid {
|
||||
let existing_projects = models::projects::project::Entity::find()
|
||||
.filter(models::projects::project::Column::CreatedBy.eq(uid))
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
if existing_projects.is_empty() {
|
||||
Decimal::from_f64_retain(DEFAULT_PROJECT_MONTHLY_CREDIT).unwrap_or(Decimal::ZERO)
|
||||
} else {
|
||||
Decimal::ZERO
|
||||
}
|
||||
} else {
|
||||
Decimal::ZERO
|
||||
};
|
||||
|
||||
let created = project_billing::ActiveModel {
|
||||
project: Set(project_uid),
|
||||
balance: Set(Decimal::from(DEFAULT_PROJECT_MONTHLY_CREDIT as i64)),
|
||||
balance: Set(initial_balance),
|
||||
currency: Set("USD".to_string()),
|
||||
user: Set(user_uid),
|
||||
updated_at: Set(now_utc),
|
||||
|
||||
@ -9,6 +9,8 @@ use serde::{Deserialize, Serialize};
|
||||
use session::Session;
|
||||
use uuid::Uuid;
|
||||
|
||||
const DEFAULT_PROJECT_INITIAL_BALANCE: f64 = 10.0;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
|
||||
pub struct ProjectInitParams {
|
||||
pub name: String,
|
||||
@ -94,9 +96,20 @@ impl AppService {
|
||||
};
|
||||
project_member.insert(&txn).await?;
|
||||
|
||||
// Only first project per user gets initial budget
|
||||
let existing_projects = project::Entity::find()
|
||||
.filter(project::Column::CreatedBy.eq(user.uid))
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
let initial_balance = if existing_projects.is_empty() {
|
||||
Decimal::from_f64_retain(DEFAULT_PROJECT_INITIAL_BALANCE).unwrap_or(Decimal::ZERO)
|
||||
} else {
|
||||
Decimal::ZERO
|
||||
};
|
||||
|
||||
let billing = project_billing::ActiveModel {
|
||||
project: Set(_project.id),
|
||||
balance: Set(Decimal::from(200i64)),
|
||||
balance: Set(initial_balance),
|
||||
currency: Set("USD".to_string()),
|
||||
user: Set(Some(user.uid)),
|
||||
updated_at: Set(Utc::now()),
|
||||
|
||||
@ -78,7 +78,7 @@ impl AppService {
|
||||
.await?
|
||||
.ok_or(AppError::NotWorkspaceMember)?;
|
||||
|
||||
let billing = self.ensure_workspace_billing(ws.id).await?;
|
||||
let billing = self.ensure_workspace_billing(ws.id, Some(user_uid)).await?;
|
||||
let now_utc = Utc::now();
|
||||
let (month_start, next_month_start) = utc_month_bounds(now_utc)?;
|
||||
|
||||
@ -132,7 +132,7 @@ impl AppService {
|
||||
let page = std::cmp::max(query.page.unwrap_or(1), 1);
|
||||
let per_page = query.per_page.unwrap_or(20).clamp(1, 200);
|
||||
|
||||
self.ensure_workspace_billing(ws.id).await?;
|
||||
self.ensure_workspace_billing(ws.id, Some(user_uid)).await?;
|
||||
|
||||
let paginator = workspace_billing_history::Entity::find()
|
||||
.filter(workspace_billing_history::Column::WorkspaceId.eq(ws.id))
|
||||
@ -186,7 +186,7 @@ impl AppService {
|
||||
return Err(AppError::BadRequest("Amount must be positive".to_string()));
|
||||
}
|
||||
|
||||
let billing = self.ensure_workspace_billing(ws.id).await?;
|
||||
let billing = self.ensure_workspace_billing(ws.id, Some(user_uid)).await?;
|
||||
let now_utc = Utc::now();
|
||||
let new_balance =
|
||||
Decimal::from_f64_retain(billing.balance.to_f64().unwrap_or_default() + params.amount)
|
||||
@ -221,6 +221,7 @@ impl AppService {
|
||||
pub async fn ensure_workspace_billing(
|
||||
&self,
|
||||
workspace_id: Uuid,
|
||||
user_uid: Option<Uuid>,
|
||||
) -> Result<workspace_billing::Model, AppError> {
|
||||
if let Some(billing) = workspace_billing::Entity::find_by_id(workspace_id)
|
||||
.one(&self.db)
|
||||
@ -230,9 +231,25 @@ impl AppService {
|
||||
}
|
||||
|
||||
let now_utc = Utc::now();
|
||||
// Only first workspace per user gets initial budget ($30)
|
||||
let initial_balance = if let Some(uid) = user_uid {
|
||||
let existing_workspaces = workspace_membership::Entity::find()
|
||||
.filter(workspace_membership::Column::UserId.eq(uid))
|
||||
.filter(workspace_membership::Column::Status.eq("active"))
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
if existing_workspaces.len() <= 1 {
|
||||
Decimal::from_f64_retain(30.0).unwrap_or(Decimal::ZERO)
|
||||
} else {
|
||||
Decimal::ZERO
|
||||
}
|
||||
} else {
|
||||
Decimal::ZERO
|
||||
};
|
||||
|
||||
let created = workspace_billing::ActiveModel {
|
||||
workspace_id: Set(workspace_id),
|
||||
balance: Set(Decimal::ZERO),
|
||||
balance: Set(initial_balance),
|
||||
currency: Set("USD".to_string()),
|
||||
monthly_quota: Set(
|
||||
Decimal::from_f64_retain(DEFAULT_MONTHLY_QUOTA).unwrap_or(Decimal::ZERO)
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
use crate::AppService;
|
||||
use crate::error::AppError;
|
||||
use chrono::Utc;
|
||||
use models::Decimal;
|
||||
use models::WorkspaceRole;
|
||||
use models::workspaces::workspace;
|
||||
use models::workspaces::workspace_billing;
|
||||
use models::workspaces::workspace_membership;
|
||||
use sea_orm::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -89,6 +91,28 @@ impl AppService {
|
||||
};
|
||||
membership.insert(&txn).await?;
|
||||
|
||||
// Create billing record — only first workspace gets $30 initial balance
|
||||
let existing_workspaces = workspace_membership::Entity::find()
|
||||
.filter(workspace_membership::Column::UserId.eq(user.uid))
|
||||
.filter(workspace_membership::Column::Status.eq("active"))
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
let initial_balance = if existing_workspaces.len() <= 1 {
|
||||
Decimal::from_f64_retain(30.0).unwrap_or(Decimal::ZERO)
|
||||
} else {
|
||||
Decimal::ZERO
|
||||
};
|
||||
let billing = workspace_billing::ActiveModel {
|
||||
workspace_id: Set(ws.id),
|
||||
balance: Set(initial_balance),
|
||||
currency: Set("USD".to_string()),
|
||||
monthly_quota: Set(Decimal::from_f64_retain(100.0).unwrap_or(Decimal::ZERO)),
|
||||
total_spent: Set(Decimal::ZERO),
|
||||
updated_at: Set(Utc::now()),
|
||||
created_at: Set(Utc::now()),
|
||||
};
|
||||
billing.insert(&txn).await?;
|
||||
|
||||
txn.commit().await?;
|
||||
Ok(ws)
|
||||
}
|
||||
|
||||
@ -193,6 +193,8 @@ export interface RoomMessagePayload {
|
||||
thread_id?: string;
|
||||
content: string;
|
||||
content_type: string;
|
||||
/** Accumulated AI reasoning/thinking text. */
|
||||
thinking_content?: string;
|
||||
send_at: string;
|
||||
seq: number;
|
||||
display_name?: string;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user