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
+9 -1
View File
@@ -8,6 +8,7 @@ SOCIAL_RUNTIME__ENVIRONMENT=dev
SOCIAL_RUNTIME__DEBUG=true
SOCIAL_RUNTIME__LOG_LEVEL=INFO
SOCIAL_RUNTIME__SQL_LOG_QUERIES=false
SOCIAL_RUNTIME__TRUSTED_PROXY_IPS=[]
############
# Web 服务器配置(Uvicorn
@@ -36,6 +37,13 @@ SOCIAL_WORKER__GROUPS__DEFAULT__CONCURRENCY=2
SOCIAL_WORKER__GROUPS__BULK__CONCURRENCY=1
############
# Automation 调度器配置
############
SOCIAL_AUTOMATION_SCHEDULER__ENABLED=true
SOCIAL_AUTOMATION_SCHEDULER__INTERVAL_SECONDS=60
SOCIAL_AUTOMATION_SCHEDULER__BATCH_LIMIT=100
############
# Taskiq(可选,默认回落到 Redis URL)
############
@@ -87,5 +95,5 @@ SOCIAL_APP_VERSION__DOWNLOAD_BASE_URL=
############
# Test相关
############
SOCIAL_TEST__EMAIL=test@xunmee.com
SOCIAL_TEST__PHONE=+8613812345678
SOCIAL_TEST__PASSWORD=Test@123456
@@ -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;
$$;
"""
)
+4
View File
@@ -162,6 +162,7 @@ ${SOCIAL_WEB__WORKERS:-2} --log-level ${UVICORN_LOG_LEVEL}"
WORKER_CRITICAL_CMD="cd '$ROOT_DIR' && PYTHONPATH=backend/src SOCIAL_RUNTIME__SERVICE_NAME=worker-critical uv run taskiq worker core.taskiq.app:critical_broker core.agentscope.runtime.tasks --workers ${SOCIAL_WORKER__GROUPS__CRITICAL__CONCURRENCY:-2}"
WORKER_DEFAULT_CMD="cd '$ROOT_DIR' && PYTHONPATH=backend/src SOCIAL_RUNTIME__SERVICE_NAME=worker-default uv run taskiq worker core.taskiq.app:default_broker core.agentscope.runtime.tasks --workers ${SOCIAL_WORKER__GROUPS__DEFAULT__CONCURRENCY:-2}"
WORKER_BULK_CMD="cd '$ROOT_DIR' && PYTHONPATH=backend/src SOCIAL_RUNTIME__SERVICE_NAME=worker-bulk uv run taskiq worker core.taskiq.app:bulk_broker core.agentscope.runtime.tasks --workers ${SOCIAL_WORKER__GROUPS__BULK__CONCURRENCY:-1}"
AUTOMATION_SCHEDULER_CMD="cd '$ROOT_DIR' && PYTHONPATH=backend/src SOCIAL_RUNTIME__SERVICE_NAME=automation-scheduler uv run python -m core.runtime.cli automation-scheduler"
echo "Starting tmux workers in session '$SESSION_NAME'..."
@@ -169,6 +170,7 @@ ${SOCIAL_WEB__WORKERS:-2} --log-level ${UVICORN_LOG_LEVEL}"
tmux new-window -t "$SESSION_NAME" -n worker-critical "bash -lc \"$WORKER_CRITICAL_CMD; echo '[worker-critical] exited'; exec bash\""
tmux new-window -t "$SESSION_NAME" -n worker-default "bash -lc \"$WORKER_DEFAULT_CMD; echo '[worker-default] exited'; exec bash\""
tmux new-window -t "$SESSION_NAME" -n worker-bulk "bash -lc \"$WORKER_BULK_CMD; echo '[worker-bulk] exited'; exec bash\""
tmux new-window -t "$SESSION_NAME" -n automation-scheduler "bash -lc \"$AUTOMATION_SCHEDULER_CMD; echo '[automation-scheduler] exited'; exec bash\""
echo ""
echo "=== App Started ==="
@@ -177,6 +179,7 @@ ${SOCIAL_WEB__WORKERS:-2} --log-level ${UVICORN_LOG_LEVEL}"
echo " - worker-critical.log, worker-critical.error.log"
echo " - worker-default.log, worker-default.error.log"
echo " - worker-bulk.log, worker-bulk.error.log"
echo " - automation-scheduler.log, automation-scheduler.error.log"
echo ""
echo "tmux attach -t $SESSION_NAME"
echo "tmux list-windows -t $SESSION_NAME"
@@ -199,6 +202,7 @@ stop() {
kill_matching_processes "uvicorn" "uv run uvicorn app:app"
kill_matching_processes "taskiq workers" "uv run taskiq worker core.taskiq.app:"
kill_matching_processes "automation scheduler" "python -m core.runtime.cli automation-scheduler"
kill_listening_processes "port ${WEB_PORT} listeners" "$WEB_PORT"