chore: 迁移到 social-app 架构,集成 Supabase 和 taskiq worker

This commit is contained in:
qzl
2026-04-02 16:36:35 +08:00
parent 695adb7d6f
commit 92cdfd9fca
132 changed files with 5802 additions and 759 deletions
-3
View File
@@ -1,3 +0,0 @@
from .settings import Settings, config
__all__ = ["Settings", "config"]
+70 -86
View File
@@ -8,6 +8,7 @@ from pydantic import (
AnyHttpUrl,
BaseModel,
Field,
SecretStr,
computed_field,
field_validator,
model_validator,
@@ -118,11 +119,54 @@ class RedisSettings(BaseModel):
return f"redis://{self.host}:{self.port}/{self.db}"
class SupabaseSettings(BaseModel):
public_url: AnyHttpUrl
anon_key: str = "CHANGE_ME"
service_role_key: str = "CHANGE_ME"
jwt_secret: SecretStr | None = Field(default=None, exclude=True)
jwt_algorithm: Literal["HS256"] = "HS256"
jwt_issuer: str | None = None
@model_validator(mode="after")
def compute_defaults(self) -> "SupabaseSettings":
base = str(self.public_url).rstrip("/")
if self.jwt_issuer is None:
self.jwt_issuer = f"{base}/auth/v1"
return self
@computed_field
@property
def url(self) -> str:
return str(self.public_url)
class StorageSettings(BaseModel):
provider: Literal["supabase"] = "supabase"
signed_url_ttl_seconds: int = Field(default=600, ge=60, le=3600)
retention_days: int = Field(default=30, ge=1, le=3650)
class AttachmentSettings(BaseModel):
bucket: str = Field(default="eryao-attachments", min_length=3, max_length=63)
max_size_mb: int = Field(default=20, ge=1, le=200)
class AvatarSettings(BaseModel):
bucket: str = Field(default="avatars", min_length=3, max_length=63)
max_size_mb: int = Field(default=2, ge=1, le=10)
attachment: AttachmentSettings = Field(default_factory=AttachmentSettings)
avatar: AvatarSettings = Field(default_factory=AvatarSettings)
class LlmSettings(BaseModel):
provider_keys: dict[str, str] = Field(default_factory=dict)
class DatabaseSettings(BaseModel):
host: str = "localhost"
port: int = 3306
name: str = "eryao"
user: str = "root"
port: int = 5432
name: str = "postgres"
user: str = "postgres"
password: str = "CHANGE_ME"
@computed_field
@@ -130,83 +174,11 @@ class DatabaseSettings(BaseModel):
def url(self) -> str:
password = quote(self.password, safe="")
return (
f"mysql+aiomysql://{self.user}:{password}"
f"postgresql+asyncpg://{self.user}:{password}"
f"@{self.host}:{self.port}/{self.name}"
)
class AppVersionSettings(BaseModel):
manifest_path: str = Field(
default="deploy/static/releases/manifest.json",
description="发布清单文件路径,相对于项目根目录",
)
release_path_prefix: str = Field(
default="releases",
description="下载 URL 中文件目录前缀",
)
download_base_url: AnyHttpUrl | None = Field(
default=None,
description="下载链接基础域名,如 https://your-domain.com",
)
@field_validator("download_base_url", mode="before")
@classmethod
def empty_download_base_url_to_none(cls, value: object) -> object:
if value == "":
return None
return value
@field_validator("manifest_path")
@classmethod
def validate_manifest_path(cls, value: str) -> str:
normalized = Path(value)
if normalized.is_absolute() or ".." in normalized.parts:
raise ValueError("manifest_path must be a safe relative path")
return value
class AliyunSmsSettings(BaseModel):
access_key_id: str = "CHANGE_ME"
access_key_secret: str = "CHANGE_ME"
sign_name: str = "CHANGE_ME"
template_code: str = "CHANGE_ME"
region_id: str = "cn-hangzhou"
endpoint: str = "dysmsapi.aliyuncs.com"
test_mode: bool = False
class AliyunContentSecuritySettings(BaseModel):
access_key_id: str = "CHANGE_ME"
access_key_secret: str = "CHANGE_ME"
endpoint: str = "green-cip.cn-shenzhen.aliyuncs.com"
class AlipaySettings(BaseModel):
app_id: str = "CHANGE_ME"
merchant_id: str = "CHANGE_ME"
public_key: str = "CHANGE_ME"
private_key: str = "CHANGE_ME"
sign_type: str = "RSA2"
notify_url: str = ""
timeout_express: str = "30m"
sandbox: bool = False
class DeepSeekSettings(BaseModel):
api_key: str = "CHANGE_ME"
class AuthSettings(BaseModel):
token_expiration_days: int = 7
token_refresh_threshold_hours: int = 2
class VerificationSettings(BaseModel):
code_length: int = 6
expiration_minutes: int = 5
test_mode: bool = False
class SensitiveWordSettings(BaseModel):
use_aliyun: bool = True
fallback_to_local: bool = True
@@ -217,6 +189,11 @@ class TestSettings(BaseModel):
password: str = ""
class TaskiqSettings(BaseModel):
broker_url: str | None = None
result_backend_url: str | None = None
def _resolve_env_file() -> str:
current = Path(__file__).resolve()
for parent in [current, *current.parents]:
@@ -233,24 +210,31 @@ class Settings(BaseSettings):
runtime: RuntimeSettings = RuntimeSettings()
cors: CorsSettings = CorsSettings()
redis: RedisSettings = RedisSettings()
database: DatabaseSettings = DatabaseSettings()
app_version: AppVersionSettings = AppVersionSettings()
aliyun_sms: AliyunSmsSettings = AliyunSmsSettings()
aliyun_content_security: AliyunContentSecuritySettings = (
AliyunContentSecuritySettings()
supabase: SupabaseSettings = Field(
default_factory=lambda: SupabaseSettings(public_url="http://localhost:8001")
)
alipay: AlipaySettings = AlipaySettings()
deepseek: DeepSeekSettings = DeepSeekSettings()
auth: AuthSettings = AuthSettings()
verification: VerificationSettings = VerificationSettings()
sensitive_word: SensitiveWordSettings = SensitiveWordSettings()
storage: StorageSettings = StorageSettings()
llm: LlmSettings = LlmSettings()
database: DatabaseSettings = DatabaseSettings()
sensitive_word: SensitiveWordSettings = Field(default_factory=SensitiveWordSettings)
test: TestSettings = Field(default_factory=TestSettings)
taskiq: TaskiqSettings = Field(default_factory=TaskiqSettings)
@computed_field
@property
def database_url(self) -> str:
return self.database.url
@computed_field
@property
def taskiq_broker_url(self) -> str:
return self.taskiq.broker_url or self.redis.url
@computed_field
@property
def taskiq_result_backend_url(self) -> str:
return self.taskiq.result_backend_url or self.redis.url
model_config: ClassVar[SettingsConfigDict] = SettingsConfigDict(
env_file=_resolve_env_file(),
env_prefix="ERYAO_",
@@ -1,34 +0,0 @@
input_template: |
你正在执行一次"自动化记忆回顾与整理"任务。
任务目标:
1) 回顾最近两天的聊天与上下文,识别用户长期偏好、习惯和关键事实的变化。
2) 对已经失效、被否定或明显过期的信息执行遗忘。
3) 对新增且有证据支持的信息执行写入。
4) 严禁编造;没有证据就不要写入。
5) 只更新最小必要字段,避免过度覆盖。
输出要求:
- 必须使用以下固定格式输出:
<----------【周期任务输出】---------->
【记忆回顾】<一句人性化总结,说明今天主要发生了什么>
【新增记忆】<按"X条:要点1;要点2"描述;没有则写"0条">
【遗忘记忆】<按"X条:要点1;要点2"描述;没有则写"0条">
【未来展望】<基于本次记忆变化,给出1-2条温和、可执行的后续建议;若暂无建议则说明"可继续观察">
表达风格:
- 语言自然、温和、可读,像助理在做每日回顾。
- 结论先行,避免空话,不要输出与任务无关的闲聊内容。
enabled_tools:
- memory.write
- memory.forget
context:
source: latest_chat
window_mode: day
window_count: 2
schedule:
type: daily
run_at:
hour: 8
minute: 0
weekdays: null
@@ -1,158 +0,0 @@
version: "1.0"
routes:
- route_id: auth.boot
path: /boot
description: Bootstraps auth session and redirects to login or home.
category: auth
auth_required: false
- route_id: auth.login
path: /login
description: Login entry for unauthenticated users.
category: auth
auth_required: false
- route_id: home.main
path: /
description: Main assistant home screen.
category: home
auth_required: true
- route_id: message.invite_list
path: /messages/invites
description: Lists message invitations.
category: messages
auth_required: true
- route_id: message.invite_detail
path: /messages/invites/{id}
description: Shows details for a single invitation.
category: messages
auth_required: true
path_params:
- id
- route_id: contacts.list
path: /contacts
description: Contact list and quick relationship actions.
category: contacts
auth_required: true
- route_id: contacts.add
path: /contacts/add
description: Create or edit a contact profile.
category: contacts
auth_required: true
- route_id: calendar.dayweek
path: /calendar/dayweek
description: Day and week calendar view.
category: calendar
auth_required: true
query_params:
- date
- from
- route_id: calendar.month
path: /calendar/month
description: Month calendar overview.
category: calendar
auth_required: true
query_params:
- from
- route_id: calendar.event_detail
path: /calendar/events/{id}
description: Detail page for one calendar event.
category: calendar
auth_required: true
path_params:
- id
- route_id: calendar.event_create
path: /calendar/events/new
description: Create page for one calendar event.
category: calendar
auth_required: true
query_params:
- date
- route_id: calendar.event_edit
path: /calendar/events/{id}/edit
description: Edit page for one calendar event.
category: calendar
auth_required: true
path_params:
- id
- route_id: calendar.event_share
path: /calendar/events/{id}/share
description: Share settings page for one calendar event.
category: calendar
auth_required: true
path_params:
- id
- route_id: todo.list
path: /todo
description: Todo quadrants and backlog overview.
category: todo
auth_required: true
- route_id: todo.create
path: /todo/new
description: Create page for one todo item.
category: todo
auth_required: true
- route_id: todo.detail
path: /todo/{id}
description: Detail page for one todo item.
category: todo
auth_required: true
path_params:
- id
- route_id: todo.edit
path: /todo/{id}/edit
description: Dedicated subpage for editing one todo item (not an in-page modal).
category: todo
auth_required: true
path_params:
- id
- route_id: settings.main
path: /settings
description: Settings hub page.
category: settings
auth_required: true
- route_id: settings.features
path: /settings/features
description: Automation job list page.
category: settings
auth_required: true
- route_id: settings.job_new
path: /settings/job/new
description: Create page for one automation job.
category: settings
auth_required: true
- route_id: settings.job_detail
path: /settings/job/{id}
description: Detail page for one automation job.
category: settings
auth_required: true
path_params:
- id
- route_id: settings.memory
path: /settings/memory
description: Memory preferences and controls.
category: settings
auth_required: true
- route_id: settings.memory_user
path: /settings/memory/user
description: User memory summary view.
category: settings
auth_required: true
- route_id: settings.memory_work
path: /settings/memory/work
description: Work memory summary view.
category: settings
auth_required: true
- route_id: settings.memory_user_edit
path: /settings/memory/user/edit
description: Edit user memory details.
category: settings
auth_required: true
- route_id: settings.memory_work_edit
path: /settings/memory/work/edit
description: Edit work memory details.
category: settings
auth_required: true
- route_id: settings.edit_profile
path: /edit-profile
description: Profile editing page.
category: settings
auth_required: true
+2 -2
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
from sqlalchemy import JSON
from sqlalchemy.dialects.mysql import JSON as MySQLJSON
from sqlalchemy.dialects.postgresql import JSONB
json_type = JSON().with_variant(MySQLJSON, "mysql")
json_jsonb = JSON().with_variant(JSONB, "postgresql")
+3
View File
@@ -0,0 +1,3 @@
from __future__ import annotations
__all__ = []
+2 -56
View File
@@ -5,9 +5,6 @@ import subprocess
import sys
from pathlib import Path
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.interval import IntervalTrigger
from core.automation.scheduler import run_automation_scheduler_scan
from core.config.initial.init_data import initialize_data
from core.config.settings import config
from core.logging import get_logger
@@ -16,7 +13,6 @@ logger = get_logger("core.runtime.cli")
def _resolve_alembic_path() -> Path:
"""Resolve alembic.ini path relative to project root."""
project_root = Path(__file__).parents[3]
alembic_path = project_root / "alembic" / "alembic.ini"
if not alembic_path.exists():
@@ -25,7 +21,6 @@ def _resolve_alembic_path() -> Path:
def _redact_sensitive(text: str) -> str:
"""Redact sensitive information from log output."""
import re
SENSITIVE_KEYS = ("password", "token", "secret", "api_key")
@@ -40,7 +35,6 @@ def _redact_sensitive(text: str) -> str:
def run_migrations() -> bool:
"""Run alembic migrations in a subprocess to avoid event loop conflicts."""
import os
logger.info("Running alembic migrations")
@@ -75,7 +69,6 @@ def run_migrations() -> bool:
async def run_init_data() -> bool:
"""Initialize bootstrap data."""
logger.info("Running init-data")
try:
result = await initialize_data()
@@ -90,7 +83,6 @@ async def run_init_data() -> bool:
async def bootstrap() -> bool:
"""Run migrations followed by init-data."""
logger.info("Starting bootstrap (migrate + init-data)")
if not run_migrations():
@@ -105,52 +97,11 @@ async def bootstrap() -> bool:
return True
async def run_automation_scheduler_forever() -> None:
if not config.automation_scheduler.enabled:
logger.info("Automation scheduler disabled by config")
return
interval_seconds = int(config.automation_scheduler.interval_seconds)
batch_limit = int(config.automation_scheduler.batch_limit)
logger.info(
"Starting automation scheduler",
interval_seconds=interval_seconds,
batch_limit=batch_limit,
)
async def scan_job() -> None:
try:
await run_automation_scheduler_scan(limit=batch_limit)
except Exception as exc:
logger.exception("Automation scheduler scan failed", error=str(exc))
scheduler = AsyncIOScheduler()
scheduler.add_job(
scan_job,
trigger=IntervalTrigger(seconds=interval_seconds),
id="automation_scheduler_scan",
name="Automation scheduler scan",
replace_existing=True,
max_instances=1,
coalesce=True,
)
scheduler.start()
stop_event = asyncio.Event()
try:
await stop_event.wait()
finally:
scheduler.shutdown(wait=False)
def main() -> int:
"""CLI entry point."""
if len(sys.argv) < 2:
logger.error("No command provided")
logger.info("Usage: python -m core.runtime.cli <command>")
logger.info(
"Available commands: migrate, init-data, bootstrap, automation-scheduler"
)
logger.info("Available commands: migrate, init-data, bootstrap")
return 1
command = sys.argv[1]
@@ -161,14 +112,9 @@ def main() -> int:
success = asyncio.run(run_init_data())
elif command == "bootstrap":
success = asyncio.run(bootstrap())
elif command == "automation-scheduler":
asyncio.run(run_automation_scheduler_forever())
return 0
else:
logger.error("Unknown command", command=command)
logger.info(
"Available commands: migrate, init-data, bootstrap, automation-scheduler"
)
logger.info("Available commands: migrate, init-data, bootstrap")
return 1
return 0 if success else 1
+3
View File
@@ -0,0 +1,3 @@
from __future__ import annotations
__all__ = []
+3
View File
@@ -0,0 +1,3 @@
from core.taskiq.app import broker, worker_agent_broker, worker_general_broker
__all__ = ["broker", "worker_agent_broker", "worker_general_broker"]
+30
View File
@@ -0,0 +1,30 @@
from __future__ import annotations
from taskiq_redis import ListQueueBroker, RedisAsyncResultBackend
from core.config.settings import config
from core.logging import configure_logging, log_service_banner
configure_logging(config)
log_service_banner(
service_name=config.runtime.service_name,
environment=config.runtime.environment,
)
def _build_broker(queue_name: str) -> ListQueueBroker:
return ListQueueBroker(
url=config.taskiq_broker_url,
queue_name=queue_name,
).with_result_backend(
RedisAsyncResultBackend(redis_url=config.taskiq_result_backend_url)
)
worker_agent_broker = _build_broker("agent")
worker_general_broker = _build_broker("general")
broker = worker_agent_broker
__all__ = ["broker", "worker_agent_broker", "worker_general_broker"]