refactor: 删除未使用的 api_external_url 配置并完善 runtime 文档

- 删除 SupabaseSettings 中未使用的 api_external_url computed field
- 更新测试文件移除相关测试用例
- backend/AGENTS.md 新增软删除设计规则
- runtime-database.md 更新表结构(删除 user_agents,表名更新为 agent_chat_sessions/messages,system_agents)
- runtime-frontend.md 补充路由结构和功能模块说明
- 根 AGENTS.md 清理过时技能路径引用
This commit is contained in:
qzl
2026-03-06 18:25:18 +08:00
parent 105e7849fe
commit 1f6cb1a48f
6 changed files with 523 additions and 320 deletions
+20
View File
@@ -164,6 +164,26 @@ Use `schemas / repository / service` pattern:
- service_role key is backend-only; never expose credentials
- Prohibit calling Supabase Admin API (service_role key) from repository/service layers
### Soft Delete
**Soft delete marks data as invisible, not cascade delete.**
- Use `deleted_at: datetime | None` column (via `SoftDeleteMixin`)
- **Query filtering**: Repository `_apply_soft_delete_filter()` auto-excludes deleted records
- **No automatic cascade**: Related data stays intact; visibility controlled by JOIN filtering
- **Cascade only for strong dependencies**: When parent deletion must invalidate children, implement in Service layer explicitly
- **Recovery**: Only restore the record itself; related data visibility restored automatically via queries
- **Unique constraints**: Use partial indexes excluding `deleted_at IS NOT NULL` to allow re-creation
```python
# Partial unique index in migration
op.execute("""
CREATE UNIQUE INDEX ux_user_email
ON users(email)
WHERE deleted_at IS NULL
""")
```
### Migrations
- **Alembic is the single source of truth** for schema migrations
-5
View File
@@ -128,11 +128,6 @@ class SupabaseSettings(BaseModel):
def public_url(self) -> str:
return f"{self.public_scheme}://{self.public_host}:{self.kong_http_port}"
@computed_field
@property
def api_external_url(self) -> str:
return self.public_url
@computed_field
@property
def url(self) -> str:
@@ -23,27 +23,13 @@ def test_social_prefixed_supabase_env_populates_settings(
settings = Settings()
assert settings.supabase.public_url == "https://public.example:8443"
assert settings.supabase.api_external_url == "https://public.example:8443"
assert settings.supabase.anon_key == "anon-key"
assert settings.supabase.service_role_key == "service-key"
assert settings.supabase.jwt_secret == "jwt-secret"
supabase_settings = settings.model_dump()["supabase"]
assert supabase_settings["public_url"] == "https://public.example:8443"
assert supabase_settings["api_external_url"] == "https://public.example:8443"
assert supabase_settings["anon_key"] == "anon-key"
assert supabase_settings["service_role_key"] == "service-key"
assert supabase_settings["jwt_secret"] == "jwt-secret"
assert settings.database_url == "postgresql+asyncpg://user:pass@db:5432/app"
def test_social_prefixed_api_external_url_is_loaded(
monkeypatch: MonkeyPatch,
) -> None:
monkeypatch.setenv("SOCIAL_SUPABASE__PUBLIC_SCHEME", "https")
monkeypatch.setenv("SOCIAL_SUPABASE__PUBLIC_HOST", "api.example")
monkeypatch.setenv("SOCIAL_SUPABASE__KONG_HTTP_PORT", "8443")
settings = Settings()
assert settings.supabase.api_external_url == "https://api.example:8443"