chore: update configuration and infrastructure
- Add SOCIAL_RUNTIME__TRUSTED_PROXY_IPS to .env.example - Add SOCIAL_AUTOMATION_SCHEDULER__* config options - Change SOCIAL_TEST__EMAIL to SOCIAL_TEST__PHONE - Update app.sh startup script - Add new database migrations for phone auth and automation
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
"""auth phone-session and profile username trigger update
|
||||
|
||||
Revision ID: 202603190002
|
||||
Revises: 20260313_0001
|
||||
Create Date: 2026-03-19 15:30:00
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "202603190002"
|
||||
down_revision: Union[str, Sequence[str], None] = "20260313_0001"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute("DROP INDEX IF EXISTS ix_profiles_referred_by")
|
||||
op.execute("ALTER TABLE profiles DROP COLUMN IF EXISTS referred_by")
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
CREATE OR REPLACE FUNCTION public.generate_profile_username_suffix(seed TEXT)
|
||||
RETURNS TEXT
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
hashed TEXT;
|
||||
BEGIN
|
||||
hashed := lower(encode(extensions.digest(seed, 'sha256'), 'base64'));
|
||||
hashed := regexp_replace(hashed, '[^a-z0-9]', '', 'g');
|
||||
IF length(hashed) < 6 THEN
|
||||
hashed := hashed || '000000';
|
||||
END IF;
|
||||
RETURN substr(hashed, 1, 6);
|
||||
END;
|
||||
$$;
|
||||
"""
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
CREATE OR REPLACE FUNCTION public.create_profile_for_new_user()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
base_seed TEXT;
|
||||
candidate_username TEXT;
|
||||
attempt INT := 0;
|
||||
BEGIN
|
||||
base_seed := coalesce(NEW.phone, NEW.id::text);
|
||||
|
||||
LOOP
|
||||
candidate_username := 'user_' || public.generate_profile_username_suffix(base_seed || ':' || attempt::text);
|
||||
EXIT WHEN NOT EXISTS (
|
||||
SELECT 1 FROM public.profiles p WHERE p.username = candidate_username
|
||||
);
|
||||
attempt := attempt + 1;
|
||||
IF attempt >= 50 THEN
|
||||
candidate_username := 'user_' || substr(replace(NEW.id::text, '-', ''), 1, 6);
|
||||
EXIT;
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
INSERT INTO public.profiles (id, username, avatar_url, bio, settings, created_at, updated_at)
|
||||
VALUES (
|
||||
NEW.id,
|
||||
candidate_username,
|
||||
NULL,
|
||||
NULL,
|
||||
'{}'::jsonb,
|
||||
now(),
|
||||
now()
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute(
|
||||
"""
|
||||
ALTER TABLE profiles ADD COLUMN referred_by UUID REFERENCES profiles(id) ON DELETE SET NULL
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"CREATE INDEX IF NOT EXISTS ix_profiles_referred_by ON profiles(referred_by)"
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
CREATE OR REPLACE FUNCTION public.create_profile_for_new_user()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
invite_code_value TEXT;
|
||||
referrer_id UUID;
|
||||
new_code TEXT;
|
||||
attempts INT := 0;
|
||||
BEGIN
|
||||
INSERT INTO public.profiles (id, username, avatar_url, bio, settings, referred_by, created_at, updated_at)
|
||||
VALUES (
|
||||
NEW.id,
|
||||
COALESCE(
|
||||
NEW.raw_user_meta_data ->> 'username',
|
||||
split_part(NEW.email, '@', 1),
|
||||
'user_' || substring(NEW.id::text, 1, 8)
|
||||
),
|
||||
NULL,
|
||||
NULL,
|
||||
'{}'::jsonb,
|
||||
NULL,
|
||||
now(),
|
||||
now()
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
LOOP
|
||||
BEGIN
|
||||
new_code := public.generate_invite_code();
|
||||
INSERT INTO public.invite_codes (code, owner_id, status, used_count, max_uses, expires_at, reward_config)
|
||||
VALUES (
|
||||
new_code,
|
||||
NEW.id,
|
||||
'active',
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
'{}'::jsonb
|
||||
);
|
||||
EXIT;
|
||||
EXCEPTION WHEN unique_violation THEN
|
||||
attempts := attempts + 1;
|
||||
IF attempts >= 100 THEN
|
||||
RAISE EXCEPTION 'Failed to generate unique invite code after 100 attempts';
|
||||
END IF;
|
||||
END;
|
||||
END LOOP;
|
||||
|
||||
invite_code_value := NEW.raw_user_meta_data ->> 'invite_code';
|
||||
IF invite_code_value IS NOT NULL AND length(invite_code_value) = 4 THEN
|
||||
invite_code_value := upper(invite_code_value);
|
||||
IF invite_code_value ~ '^[ABCDEFGHJKMNPQRSTUVWXYZ23456789]{4}$' THEN
|
||||
UPDATE public.invite_codes
|
||||
SET used_count = used_count + 1
|
||||
WHERE code = invite_code_value
|
||||
AND status = 'active'
|
||||
AND (max_uses IS NULL OR used_count < max_uses)
|
||||
AND (expires_at IS NULL OR expires_at > NOW())
|
||||
RETURNING owner_id INTO referrer_id;
|
||||
|
||||
IF referrer_id IS NOT NULL THEN
|
||||
UPDATE public.profiles
|
||||
SET referred_by = referrer_id
|
||||
WHERE id = NEW.id;
|
||||
END IF;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
"""
|
||||
)
|
||||
|
||||
op.execute("DROP FUNCTION IF EXISTS public.generate_profile_username_suffix(TEXT)")
|
||||
@@ -0,0 +1,43 @@
|
||||
"""add_messages_visibility_mask
|
||||
|
||||
Revision ID: 20260319_0003
|
||||
Revises: 202603190002
|
||||
Create Date: 2026-03-19 18:10:00
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "20260319_0003"
|
||||
down_revision: str | Sequence[str] | None = "202603190002"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"messages",
|
||||
sa.Column(
|
||||
"visibility_mask",
|
||||
sa.BigInteger(),
|
||||
nullable=False,
|
||||
server_default=sa.text("0"),
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_messages_session_seq_visibility",
|
||||
"messages",
|
||||
["session_id", "seq", "visibility_mask"],
|
||||
unique=False,
|
||||
)
|
||||
op.execute("UPDATE messages SET visibility_mask = 1 WHERE visibility_mask = 0")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_messages_session_seq_visibility", table_name="messages")
|
||||
op.drop_column("messages", "visibility_mask")
|
||||
@@ -0,0 +1,237 @@
|
||||
"""automation_job_config_for_memory
|
||||
|
||||
Revision ID: 20260319_0004
|
||||
Revises: 20260319_0003
|
||||
Create Date: 2026-03-19 20:10:00
|
||||
"""
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
|
||||
revision: str = "20260319_0004"
|
||||
down_revision: str | Sequence[str] | None = "20260319_0003"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"automation_jobs",
|
||||
sa.Column(
|
||||
"config",
|
||||
postgresql.JSONB(astext_type=sa.Text()),
|
||||
nullable=False,
|
||||
server_default=sa.text("'{}'::jsonb"),
|
||||
),
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE automation_jobs
|
||||
SET config = jsonb_build_object(
|
||||
'agent_type', 'memory',
|
||||
'model_code', 'qwen3.5-flash',
|
||||
'enabled_tools', jsonb_build_array('calendar.read', 'user.lookup'),
|
||||
'input_template', prompt,
|
||||
'context', jsonb_build_object(
|
||||
'source', 'latest_chat',
|
||||
'window_mode', 'day',
|
||||
'window_count', 2
|
||||
)
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
op.drop_column("automation_jobs", "prompt")
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
WITH ranked AS (
|
||||
SELECT
|
||||
id,
|
||||
row_number() OVER (
|
||||
PARTITION BY owner_id
|
||||
ORDER BY updated_at DESC, created_at DESC, id DESC
|
||||
) AS rn
|
||||
FROM public.automation_jobs
|
||||
WHERE deleted_at IS NULL
|
||||
AND config->>'agent_type' = 'memory'
|
||||
)
|
||||
UPDATE public.automation_jobs aj
|
||||
SET deleted_at = now()
|
||||
FROM ranked r
|
||||
WHERE aj.id = r.id
|
||||
AND r.rn > 1
|
||||
"""
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS ux_automation_jobs_owner_memory_active
|
||||
ON public.automation_jobs(owner_id)
|
||||
WHERE deleted_at IS NULL
|
||||
AND config->>'agent_type' = 'memory'
|
||||
"""
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
CREATE OR REPLACE FUNCTION public.create_profile_for_new_user()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
base_seed TEXT;
|
||||
candidate_username TEXT;
|
||||
attempt INT := 0;
|
||||
BEGIN
|
||||
base_seed := coalesce(NEW.phone, NEW.id::text);
|
||||
|
||||
LOOP
|
||||
candidate_username := 'user_' || public.generate_profile_username_suffix(base_seed || ':' || attempt::text);
|
||||
EXIT WHEN NOT EXISTS (
|
||||
SELECT 1 FROM public.profiles p WHERE p.username = candidate_username
|
||||
);
|
||||
attempt := attempt + 1;
|
||||
IF attempt >= 50 THEN
|
||||
candidate_username := 'user_' || substr(replace(NEW.id::text, '-', ''), 1, 6);
|
||||
EXIT;
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
INSERT INTO public.profiles (id, username, avatar_url, bio, settings, created_at, updated_at)
|
||||
VALUES (
|
||||
NEW.id,
|
||||
candidate_username,
|
||||
NULL,
|
||||
NULL,
|
||||
'{}'::jsonb,
|
||||
now(),
|
||||
now()
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM public.automation_jobs aj
|
||||
WHERE aj.owner_id = NEW.id
|
||||
AND aj.deleted_at IS NULL
|
||||
AND aj.config->>'agent_type' = 'memory'
|
||||
) THEN
|
||||
INSERT INTO public.automation_jobs (
|
||||
id,
|
||||
owner_id,
|
||||
title,
|
||||
config,
|
||||
schedule_type,
|
||||
run_at,
|
||||
next_run_at,
|
||||
timezone,
|
||||
status,
|
||||
created_by,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
gen_random_uuid(),
|
||||
NEW.id,
|
||||
'Memory Agent',
|
||||
jsonb_build_object(
|
||||
'agent_type', 'memory',
|
||||
'model_code', 'qwen3.5-flash',
|
||||
'enabled_tools', jsonb_build_array('calendar.read', 'user.lookup'),
|
||||
'input_template', '请基于最近聊天上下文生成一段可执行的记忆总结与建议。',
|
||||
'context', jsonb_build_object(
|
||||
'source', 'latest_chat',
|
||||
'window_mode', 'day',
|
||||
'window_count', 2
|
||||
)
|
||||
),
|
||||
'daily',
|
||||
now(),
|
||||
now() + interval '1 day',
|
||||
'UTC',
|
||||
'active',
|
||||
NEW.id,
|
||||
now(),
|
||||
now()
|
||||
);
|
||||
END IF;
|
||||
EXCEPTION WHEN unique_violation THEN
|
||||
NULL;
|
||||
END;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DROP INDEX IF EXISTS ux_automation_jobs_owner_memory_active")
|
||||
|
||||
op.add_column(
|
||||
"automation_jobs",
|
||||
sa.Column("prompt", sa.Text(), nullable=False, server_default=sa.text("''")),
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE automation_jobs
|
||||
SET prompt = COALESCE(config->>'input_template', '')
|
||||
"""
|
||||
)
|
||||
|
||||
op.drop_column("automation_jobs", "config")
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
CREATE OR REPLACE FUNCTION public.create_profile_for_new_user()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
base_seed TEXT;
|
||||
candidate_username TEXT;
|
||||
attempt INT := 0;
|
||||
BEGIN
|
||||
base_seed := coalesce(NEW.phone, NEW.id::text);
|
||||
|
||||
LOOP
|
||||
candidate_username := 'user_' || public.generate_profile_username_suffix(base_seed || ':' || attempt::text);
|
||||
EXIT WHEN NOT EXISTS (
|
||||
SELECT 1 FROM public.profiles p WHERE p.username = candidate_username
|
||||
);
|
||||
attempt := attempt + 1;
|
||||
IF attempt >= 50 THEN
|
||||
candidate_username := 'user_' || substr(replace(NEW.id::text, '-', ''), 1, 6);
|
||||
EXIT;
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
INSERT INTO public.profiles (id, username, avatar_url, bio, settings, created_at, updated_at)
|
||||
VALUES (
|
||||
NEW.id,
|
||||
candidate_username,
|
||||
NULL,
|
||||
NULL,
|
||||
'{}'::jsonb,
|
||||
now(),
|
||||
now()
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
"""
|
||||
)
|
||||
Reference in New Issue
Block a user