chore: checkpoint ai search comments and i18n foundation

This commit is contained in:
2026-03-28 17:17:31 +08:00
parent d18a709987
commit ec96d91548
71 changed files with 9494 additions and 423 deletions

View File

@@ -13,6 +13,10 @@ mod m20260328_000002_create_site_settings;
mod m20260328_000003_add_site_url_to_site_settings;
mod m20260328_000004_add_posts_search_index;
mod m20260328_000005_categories;
mod m20260328_000006_add_ai_to_site_settings;
mod m20260328_000007_create_ai_chunks;
mod m20260328_000008_enable_pgvector_for_ai_chunks;
mod m20260328_000009_add_paragraph_comments;
pub struct Migrator;
#[async_trait::async_trait]
@@ -30,6 +34,10 @@ impl MigratorTrait for Migrator {
Box::new(m20260328_000003_add_site_url_to_site_settings::Migration),
Box::new(m20260328_000004_add_posts_search_index::Migration),
Box::new(m20260328_000005_categories::Migration),
Box::new(m20260328_000006_add_ai_to_site_settings::Migration),
Box::new(m20260328_000007_create_ai_chunks::Migration),
Box::new(m20260328_000008_enable_pgvector_for_ai_chunks::Migration),
Box::new(m20260328_000009_add_paragraph_comments::Migration),
// inject-above (do not remove this comment)
]
}

View File

@@ -0,0 +1,175 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let table = Alias::new("site_settings");
if !manager.has_column("site_settings", "ai_enabled").await? {
manager
.alter_table(
Table::alter()
.table(table.clone())
.add_column(
ColumnDef::new(Alias::new("ai_enabled"))
.boolean()
.null()
.default(false),
)
.to_owned(),
)
.await?;
}
if !manager.has_column("site_settings", "ai_provider").await? {
manager
.alter_table(
Table::alter()
.table(table.clone())
.add_column(ColumnDef::new(Alias::new("ai_provider")).string().null())
.to_owned(),
)
.await?;
}
if !manager.has_column("site_settings", "ai_api_base").await? {
manager
.alter_table(
Table::alter()
.table(table.clone())
.add_column(ColumnDef::new(Alias::new("ai_api_base")).string().null())
.to_owned(),
)
.await?;
}
if !manager.has_column("site_settings", "ai_api_key").await? {
manager
.alter_table(
Table::alter()
.table(table.clone())
.add_column(ColumnDef::new(Alias::new("ai_api_key")).text().null())
.to_owned(),
)
.await?;
}
if !manager.has_column("site_settings", "ai_chat_model").await? {
manager
.alter_table(
Table::alter()
.table(table.clone())
.add_column(ColumnDef::new(Alias::new("ai_chat_model")).string().null())
.to_owned(),
)
.await?;
}
if !manager
.has_column("site_settings", "ai_embedding_model")
.await?
{
manager
.alter_table(
Table::alter()
.table(table.clone())
.add_column(
ColumnDef::new(Alias::new("ai_embedding_model"))
.string()
.null(),
)
.to_owned(),
)
.await?;
}
if !manager
.has_column("site_settings", "ai_system_prompt")
.await?
{
manager
.alter_table(
Table::alter()
.table(table.clone())
.add_column(ColumnDef::new(Alias::new("ai_system_prompt")).text().null())
.to_owned(),
)
.await?;
}
if !manager.has_column("site_settings", "ai_top_k").await? {
manager
.alter_table(
Table::alter()
.table(table.clone())
.add_column(ColumnDef::new(Alias::new("ai_top_k")).integer().null())
.to_owned(),
)
.await?;
}
if !manager.has_column("site_settings", "ai_chunk_size").await? {
manager
.alter_table(
Table::alter()
.table(table.clone())
.add_column(ColumnDef::new(Alias::new("ai_chunk_size")).integer().null())
.to_owned(),
)
.await?;
}
if !manager
.has_column("site_settings", "ai_last_indexed_at")
.await?
{
manager
.alter_table(
Table::alter()
.table(table)
.add_column(
ColumnDef::new(Alias::new("ai_last_indexed_at"))
.timestamp_with_time_zone()
.null(),
)
.to_owned(),
)
.await?;
}
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let table = Alias::new("site_settings");
for column in [
"ai_last_indexed_at",
"ai_chunk_size",
"ai_top_k",
"ai_system_prompt",
"ai_embedding_model",
"ai_chat_model",
"ai_api_key",
"ai_api_base",
"ai_provider",
"ai_enabled",
] {
if manager.has_column("site_settings", column).await? {
manager
.alter_table(
Table::alter()
.table(table.clone())
.drop_column(Alias::new(column))
.to_owned(),
)
.await?;
}
}
Ok(())
}
}

View File

@@ -0,0 +1,33 @@
use loco_rs::schema::*;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
create_table(
manager,
"ai_chunks",
&[
("id", ColType::PkAuto),
("source_slug", ColType::String),
("source_title", ColType::StringNull),
("source_path", ColType::StringNull),
("source_type", ColType::String),
("chunk_index", ColType::Integer),
("content", ColType::Text),
("content_preview", ColType::StringNull),
("embedding", ColType::JsonBinaryNull),
("word_count", ColType::IntegerNull),
],
&[],
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
drop_table(manager, "ai_chunks").await
}
}

View File

@@ -0,0 +1,69 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
const AI_CHUNKS_TABLE: &str = "ai_chunks";
const VECTOR_INDEX_NAME: &str = "idx_ai_chunks_embedding_hnsw";
const EMBEDDING_DIMENSION: i32 = 384;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.get_connection()
.execute_unprepared("CREATE EXTENSION IF NOT EXISTS vector")
.await?;
if manager.has_column(AI_CHUNKS_TABLE, "embedding").await? {
manager
.get_connection()
.execute_unprepared("ALTER TABLE ai_chunks DROP COLUMN embedding")
.await?;
}
if !manager.has_column(AI_CHUNKS_TABLE, "embedding").await? {
manager
.get_connection()
.execute_unprepared(&format!(
"ALTER TABLE ai_chunks ADD COLUMN embedding vector({EMBEDDING_DIMENSION})"
))
.await?;
}
manager
.get_connection()
.execute_unprepared("TRUNCATE TABLE ai_chunks RESTART IDENTITY")
.await?;
manager
.get_connection()
.execute_unprepared(&format!(
"CREATE INDEX IF NOT EXISTS {VECTOR_INDEX_NAME} ON ai_chunks USING hnsw (embedding vector_cosine_ops)"
))
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.get_connection()
.execute_unprepared(&format!("DROP INDEX IF EXISTS {VECTOR_INDEX_NAME}"))
.await?;
if manager.has_column(AI_CHUNKS_TABLE, "embedding").await? {
manager
.get_connection()
.execute_unprepared("ALTER TABLE ai_chunks DROP COLUMN embedding")
.await?;
}
manager
.get_connection()
.execute_unprepared("ALTER TABLE ai_chunks ADD COLUMN embedding jsonb")
.await?;
Ok(())
}
}

View File

@@ -0,0 +1,109 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
const TABLE: &str = "comments";
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let table = Alias::new(TABLE);
if !manager.has_column(TABLE, "scope").await? {
manager
.alter_table(
Table::alter()
.table(table.clone())
.add_column(
ColumnDef::new(Alias::new("scope"))
.string()
.not_null()
.default("article"),
)
.to_owned(),
)
.await?;
}
if !manager.has_column(TABLE, "paragraph_key").await? {
manager
.alter_table(
Table::alter()
.table(table.clone())
.add_column(ColumnDef::new(Alias::new("paragraph_key")).string().null())
.to_owned(),
)
.await?;
}
if !manager.has_column(TABLE, "paragraph_excerpt").await? {
manager
.alter_table(
Table::alter()
.table(table.clone())
.add_column(ColumnDef::new(Alias::new("paragraph_excerpt")).string().null())
.to_owned(),
)
.await?;
}
if !manager.has_column(TABLE, "reply_to_comment_id").await? {
manager
.alter_table(
Table::alter()
.table(table.clone())
.add_column(
ColumnDef::new(Alias::new("reply_to_comment_id"))
.integer()
.null(),
)
.to_owned(),
)
.await?;
}
manager
.get_connection()
.execute_unprepared(
"UPDATE comments SET scope = 'article' WHERE scope IS NULL OR trim(scope) = ''",
)
.await?;
manager
.get_connection()
.execute_unprepared(
"CREATE INDEX IF NOT EXISTS idx_comments_post_scope_paragraph ON comments (post_slug, scope, paragraph_key)",
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.get_connection()
.execute_unprepared("DROP INDEX IF EXISTS idx_comments_post_scope_paragraph")
.await?;
for column in [
"reply_to_comment_id",
"paragraph_excerpt",
"paragraph_key",
"scope",
] {
if manager.has_column(TABLE, column).await? {
manager
.alter_table(
Table::alter()
.table(Alias::new(TABLE))
.drop_column(Alias::new(column))
.to_owned(),
)
.await?;
}
}
Ok(())
}
}