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:
qzl
2026-03-19 18:43:15 +08:00
parent 8d4a14150b
commit 9ddd7a0147
5 changed files with 472 additions and 1 deletions
@@ -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;
$$;
"""
)