diff --git a/docs/plans/2026-03-02-bugfix-plan.md b/docs/plans/2026-03-02-bugfix-plan.md new file mode 100644 index 0000000..9536523 --- /dev/null +++ b/docs/plans/2026-03-02-bugfix-plan.md @@ -0,0 +1,806 @@ +# Bug Fix Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** 修复 TRIGGER-001 (user_agents 自动创建) 和 TOKEN-001 (Flutter 硬编码颜色迁移) + +**Architecture:** +- TRIGGER-001: YAML 配置 → catalog 表 → Trigger 自动创建 +- TOKEN-001: 渐进式迁移硬编码颜色到 AppColors + +**Tech Stack:** Python, PostgreSQL, Alembic, Dart, Flutter + +--- + +## TRIGGER-001: user_agents 自动创建 + +### Task 1: 创建 user_agent_catalog.yaml 配置文件 + +**Files:** +- Modify: `backend/src/core/config/static/database/user_agent_catalog.yaml` + +**Step 1: 填充 YAML 配置** + +```yaml +agents: + - agent_type: INTENT_RECOGNITION + llm_model_code: qwen3.5-flash + status: active + config: + temperature: 0.7 + + - agent_type: TASK_EXECUTION + llm_model_code: deepseek-v3.2 + status: active + config: + temperature: 0.7 + + - agent_type: RESULT_REPORTING + llm_model_code: deepseek-v3.2 + status: active + config: + temperature: 0.7 +``` + +**Step 2: 验证 YAML 语法** + +Run: `cat backend/src/core/config/static/database/user_agent_catalog.yaml` + +Expected: YAML 格式正确,包含 3 个 agent 配置 + +**Step 3: Commit** + +```bash +git add backend/src/core/config/static/database/user_agent_catalog.yaml +git commit -m "feat(config): add user_agent_catalog.yaml with default agent configs" +``` + +--- + +### Task 2: 创建 UserAgentCatalog 模型 + +**Files:** +- Create: `backend/src/models/user_agent_catalog.py` +- Modify: `backend/src/models/__init__.py` + +**Step 1: 创建模型文件** + +```python +# backend/src/models/user_agent_catalog.py +from __future__ import annotations + +from sqlalchemy import ForeignKey, String +from sqlalchemy.dialects.postgresql import JSONB, UUID +from sqlalchemy.orm import Mapped, mapped_column + +from core.db.base import Base, TimestampMixin + + +class UserAgentCatalog(TimestampMixin, Base): + __tablename__ = "user_agent_catalog" + + agent_type: Mapped[str] = mapped_column( + String(20), + primary_key=True, + ) + llm_id: Mapped[str] = mapped_column( + UUID(as_uuid=True), + ForeignKey("llms.id", ondelete="RESTRICT"), + nullable=False, + ) + status: Mapped[str] = mapped_column( + String(20), + nullable=False, + ) + config: Mapped[dict] = mapped_column( + JSONB, + nullable=False, + server_default="{}", + ) +``` + +**Step 2: 在 __init__.py 中导出** + +```python +# backend/src/models/__init__.py (添加) +from models.user_agent_catalog import UserAgentCatalog + +__all__ = [ + # ... 现有导出 + "UserAgentCatalog", +] +``` + +**Step 3: 运行类型检查** + +Run: `cd backend && uv run basedpyright src/models/user_agent_catalog.py` + +Expected: No errors + +**Step 4: Commit** + +```bash +git add backend/src/models/user_agent_catalog.py backend/src/models/__init__.py +git commit -m "feat(models): add UserAgentCatalog model" +``` + +--- + +### Task 3: 创建 Alembic 迁移 + +**Files:** +- Create: `backend/alembic/versions/20260302_add_user_agent_catalog.py` + +**Step 1: 生成迁移文件** + +Run: `cd backend && uv run alembic revision -m "add_user_agent_catalog"` + +**Step 2: 编写迁移逻辑** + +```python +# backend/alembic/versions/20260302_add_user_agent_catalog.py +"""add user_agent_catalog table + +Revision ID: 202603020001 +Revises: +Create Date: 2026-03-02 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +revision: str = "202603020001" +down_revision: Union[str, Sequence[str], None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # 创建 user_agent_catalog 表 + op.create_table( + "user_agent_catalog", + sa.Column("agent_type", sa.String(20), nullable=False), + sa.Column("llm_id", sa.UUID(), nullable=False), + sa.Column("status", sa.String(20), nullable=False), + sa.Column( + "config", + postgresql.JSONB(astext_type=sa.Text()), + server_default="{}", + nullable=False, + ), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.PrimaryKeyConstraint("agent_type"), + sa.ForeignKeyConstraint( + ["llm_id"], ["llms.id"], name="fk_user_agent_catalog_llm_id", ondelete="RESTRICT" + ), + ) + + op.execute( + "ALTER TABLE user_agent_catalog " + "ADD CONSTRAINT chk_user_agent_catalog_status " + "CHECK (status IN ('active', 'paused', 'migrating'))" + ) + + _enable_rls("user_agent_catalog") + + # 替换 trigger 函数 + op.execute(""" + CREATE OR REPLACE FUNCTION public.create_profile_for_new_user() + RETURNS trigger + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path = public + AS $$ + BEGIN + INSERT INTO public.profiles (id, username, avatar_url, bio, settings, 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, + now(), + now() + ) + ON CONFLICT (id) DO NOTHING; + + INSERT INTO public.user_agents (id, user_id, llm_id, agent_type, config, status, created_by, updated_by) + SELECT + gen_random_uuid(), + NEW.id, + uac.llm_id, + uac.agent_type, + uac.config, + uac.status, + NEW.id, + NEW.id + FROM public.user_agent_catalog uac; + + RETURN NEW; + END; + $$; + """) + + +def downgrade() -> None: + # 恢复旧 trigger 函数 + op.execute(""" + CREATE OR REPLACE FUNCTION public.create_profile_for_new_user() + RETURNS trigger + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path = public + AS $$ + BEGIN + INSERT INTO public.profiles (id, username, avatar_url, bio, settings, 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, + now(), + now() + ) + ON CONFLICT (id) DO NOTHING; + + RETURN NEW; + END; + $$; + """) + + _drop_rls("user_agent_catalog") + op.drop_constraint("chk_user_agent_catalog_status", "user_agent_catalog", type_="check") + op.drop_constraint("fk_user_agent_catalog_llm_id", "user_agent_catalog", type_="foreignkey") + op.drop_table("user_agent_catalog") + + +def _enable_rls(table_name: str) -> None: + for role in ["anon", "authenticated"]: + for action in ["select", "insert", "update", "delete"]: + op.execute( + f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}" + ) + op.execute(f"ALTER TABLE {table_name} ENABLE ROW LEVEL SECURITY") + for role in ["anon", "authenticated"]: + op.execute( + f"CREATE POLICY {role}_select_{table_name} ON {table_name} FOR SELECT TO {role} USING (false)" + ) + op.execute( + f"CREATE POLICY {role}_insert_{table_name} ON {table_name} FOR INSERT TO {role} WITH CHECK (false)" + ) + op.execute( + f"CREATE POLICY {role}_update_{table_name} ON {table_name} FOR UPDATE TO {role} USING (false) WITH CHECK (false)" + ) + op.execute( + f"CREATE POLICY {role}_delete_{table_name} ON {table_name} FOR DELETE TO {role} USING (false)" + ) + + +def _drop_rls(table_name: str) -> None: + for role in ["anon", "authenticated"]: + op.execute(f"DROP POLICY IF EXISTS {role}_delete_{table_name} ON {table_name}") + op.execute(f"DROP POLICY IF EXISTS {role}_update_{table_name} ON {table_name}") + op.execute(f"DROP POLICY IF EXISTS {role}_insert_{table_name} ON {table_name}") + op.execute(f"DROP POLICY IF EXISTS {role}_select_{table_name} ON {table_name}") + op.execute(f"ALTER TABLE {table_name} DISABLE ROW LEVEL SECURITY") +``` + +**Step 3: 运行迁移检查** + +Run: `cd backend && uv run alembic check` + +Expected: No issues + +**Step 4: Commit** + +```bash +git add backend/alembic/versions/20260302_add_user_agent_catalog.py +git commit -m "feat(migration): add user_agent_catalog table and update trigger" +``` + +--- + +### Task 4: 扩展初始化函数 + +**Files:** +- Modify: `backend/src/core/config/initial/init_data.py` + +**Step 1: 添加 Pydantic 模型** + +```python +# backend/src/core/config/initial/init_data.py (添加到文件顶部) +from models.user_agent_catalog import UserAgentCatalog + + +class UserAgentCatalogSeed(BaseModel): + agent_type: str + llm_model_code: str + status: str + config: dict[str, Any] + + +class UserAgentCatalogYaml(BaseModel): + agents: list[UserAgentCatalogSeed] +``` + +**Step 2: 添加 load 函数** + +```python +# backend/src/core/config/initial/init_data.py (添加) + +def _default_user_agent_catalog_path() -> Path: + return ( + Path(__file__).resolve().parents[1] / "static" / "database" / "user_agent_catalog.yaml" + ) + + +def load_user_agent_catalog(catalog_path: Path | None = None) -> dict[str, Any]: + path = catalog_path or _default_user_agent_catalog_path() + with path.open("r", encoding="utf-8") as file: + loaded = yaml.safe_load(file) or {} + if not isinstance(loaded, dict): + raise ValueError(f"Invalid user agent catalog format: {path}") + raw_agents = loaded.get("agents", []) + if not isinstance(raw_agents, list): + raise ValueError(f"Invalid user agent catalog agents section: {path}") + try: + parsed = UserAgentCatalogYaml.model_validate({"agents": list(raw_agents)}) + except ValidationError as exc: + raise ValueError(f"Invalid user agent catalog data: {path}") from exc + + return parsed.model_dump() +``` + +**Step 3: 添加初始化函数** + +```python +# backend/src/core/config/initial/init_data.py (添加) + +async def _upsert_user_agent_catalog( + session: AsyncSession, + *, + agent_type: str, + llm_id: uuid.UUID, + status: str, + config: dict[str, Any], +) -> None: + result = await session.execute( + select(UserAgentCatalog).where(UserAgentCatalog.agent_type == agent_type) + ) + catalog_entry = result.scalar_one_or_none() + + if catalog_entry is None: + session.add(UserAgentCatalog( + agent_type=agent_type, + llm_id=llm_id, + status=status, + config=config, + )) + else: + catalog_entry.llm_id = llm_id + catalog_entry.status = status + catalog_entry.config = config + + +async def initialize_user_agent_catalog() -> None: + """Initialize user agent catalog from YAML.""" + catalog = load_user_agent_catalog() + + async with AsyncSessionLocal() as session: + async with session.begin(): + for agent in catalog["agents"]: + # 查找 llm_id + result = await session.execute( + select(Llm).where(Llm.model_code == agent["llm_model_code"]) + ) + llm = result.scalar_one_or_none() + if llm is None: + raise RuntimeError( + f"LLM model '{agent['llm_model_code']}' not found for agent type '{agent['agent_type']}'" + ) + + await _upsert_user_agent_catalog( + session, + agent_type=agent["agent_type"], + llm_id=llm.id, + status=agent["status"], + config=agent["config"], + ) + + logger.info("Initialized user agent catalog") +``` + +**Step 4: 更新 initialize_data 函数** + +```python +# backend/src/core/config/initial/init_data.py (修改) + +async def initialize_data() -> bool: + """Initialize bootstrap data.""" + await initialize_llm_catalog() + await initialize_user_agent_catalog() # 新增 + + return True +``` + +**Step 5: 运行类型检查** + +Run: `cd backend && uv run basedpyright src/core/config/initial/init_data.py` + +Expected: No errors + +**Step 6: Commit** + +```bash +git add backend/src/core/config/initial/init_data.py +git commit -m "feat(init): add user_agent_catalog initialization" +``` + +--- + +### Task 5: 编写单元测试 + +**Files:** +- Modify: `backend/tests/unit/core/test_agent_init_data.py` + +**Step 1: 添加测试** + +```python +# backend/tests/unit/core/test_agent_init_data.py (添加到文件末尾) + +def test_user_agent_catalog_file_exists_and_has_required_fields() -> None: + catalog_path = ( + Path(__file__).resolve().parents[4] + / "src" + / "core" + / "config" + / "static" + / "database" + / "user_agent_catalog.yaml" + ) + + assert catalog_path.exists(), f"Catalog file not found: {catalog_path}" + + catalog = init_data.load_user_agent_catalog(catalog_path) + + assert "agents" in catalog + assert isinstance(catalog["agents"], list) + assert len(catalog["agents"]) == 3 + + for agent in catalog["agents"]: + assert "agent_type" in agent + assert "llm_model_code" in agent + assert "status" in agent + assert "config" in agent + assert isinstance(agent["config"], dict) + + +def test_load_user_agent_catalog_raises_on_invalid_structure(tmp_path: Path) -> None: + catalog_path = tmp_path / "user_agent_catalog.yaml" + catalog_path.write_text("invalid: structure\n") + + with pytest.raises(ValueError, match="Invalid user agent catalog"): + init_data.load_user_agent_catalog(catalog_path) +``` + +**Step 2: 运行测试** + +Run: `cd backend && uv run pytest tests/unit/core/test_agent_init_data.py -v` + +Expected: All tests pass + +**Step 3: Commit** + +```bash +git add backend/tests/unit/core/test_agent_init_data.py +git commit -m "test(init): add user_agent_catalog validation tests" +``` + +--- + +### Task 6: 运行迁移并验证 + +**Files:** +- None (database operations) + +**Step 1: 运行迁移** + +Run: `cd backend && uv run alembic upgrade head` + +Expected: Migration succeeds + +**Step 2: 运行初始化** + +Run: `cd backend && uv run python -m core.runtime.cli init-data` + +Expected: "Initialized user agent catalog" in output + +**Step 3: 验证数据库** + +Run: `docker exec -it social-supabase-db psql -U postgres -d postgres -c "SELECT * FROM user_agent_catalog;"` + +Expected: 3 rows (INTENT_RECOGNITION, TASK_EXECUTION, RESULT_REPORTING) + +**Step 4: 测试 Trigger(手动)** + +```sql +-- 创建测试用户 +INSERT INTO auth.users (id, email, raw_user_meta_data) +VALUES (gen_random_uuid(), 'test@example.com', '{"username": "testuser"}'); + +-- 验证 profiles 和 user_agents 自动创建 +SELECT * FROM profiles WHERE username = 'testuser'; +SELECT * FROM user_agents WHERE user_id = (SELECT id FROM profiles WHERE username = 'testuser'); + +-- 清理测试数据 +DELETE FROM user_agents WHERE user_id = (SELECT id FROM profiles WHERE username = 'testuser'); +DELETE FROM profiles WHERE username = 'testuser'; +DELETE FROM auth.users WHERE email = 'test@example.com'; +``` + +Expected: profiles 有 1 条,user_agents 有 3 条 + +--- + +### Task 7: 为已存在用户补充 user_agents + +**Files:** +- None (database operations) + +**Step 1: 执行补充脚本** + +```sql +-- 为已存在但没有 user_agents 的用户补充记录 +INSERT INTO user_agents (id, user_id, llm_id, agent_type, config, status, created_by, updated_by) +SELECT + gen_random_uuid(), + p.id, + uac.llm_id, + uac.agent_type, + uac.config, + uac.status, + p.id, + p.id +FROM profiles p +CROSS JOIN user_agent_catalog uac +WHERE NOT EXISTS ( + SELECT 1 FROM user_agents ua WHERE ua.user_id = p.id +); +``` + +**Step 2: 验证结果** + +Run: `docker exec -it social-supabase-db psql -U postgres -d postgres -c "SELECT user_id, COUNT(*) FROM user_agents GROUP BY user_id;"` + +Expected: 每个用户有 3 条 user_agents 记录 + +--- + +## TOKEN-001: Flutter 硬编码颜色迁移 + +### Task 8: 扫描硬编码颜色 + +**Files:** +- None (analysis only) + +**Step 1: 扫描所有硬编码颜色** + +Run: +```bash +cd apps && grep -r "Color(0x" lib --include="*.dart" | \ + sed 's/.*Color(0x\([0-9A-Fa-f]*\)).*/\1/' | \ + sort | uniq -c | sort -rn > /tmp/color_stats.txt && \ + cat /tmp/color_stats.txt +``` + +Expected: 统计结果输出,显示每个颜色的使用频率 + +**Step 2: 记录统计结果** + +将统计结果保存到临时文件供后续使用。 + +--- + +### Task 9: 扩展 AppColors + +**Files:** +- Modify: `apps/lib/core/theme/design_tokens.dart` + +**Step 1: 添加新颜色常量** + +```dart +// apps/lib/core/theme/design_tokens.dart (在 AppColors 类中添加) + +class AppColors { + // 现有颜色... + + // 从硬编码迁移的颜色(根据 Task 8 的统计结果) + static const Color linkBlue = Color(0xFF2196F3); + static const Color errorRed = Color(0xFFD32F2F); + static const Color successGreen = Color(0xFF4CAF50); + static const Color warningOrange = Color(0xFFFF9800); + // ... 根据统计结果添加其他高频颜色 +} +``` + +**Step 2: 运行 Flutter 分析** + +Run: `cd apps && flutter analyze lib/core/theme/design_tokens.dart` + +Expected: No issues + +**Step 3: Commit** + +```bash +git add apps/lib/core/theme/design_tokens.dart +git commit -m "feat(theme): add migrated colors to AppColors" +``` + +--- + +### Task 10: 迁移 register_screen.dart + +**Files:** +- Modify: `apps/lib/features/auth/ui/screens/register_screen.dart` + +**Step 1: 替换硬编码颜色** + +```dart +// Before +Container(color: Color(0xFF2196F3)) + +// After +Container(color: AppColors.linkBlue) +``` + +**Step 2: 检查所有实例** + +Run: `cd apps && grep "Color(0x" lib/features/auth/ui/screens/register_screen.dart` + +Expected: No matches (或只剩下合理的动态颜色) + +**Step 3: 运行 Flutter 分析** + +Run: `cd apps && flutter analyze lib/features/auth/ui/screens/register_screen.dart` + +Expected: No issues + +**Step 4: Commit** + +```bash +git add apps/lib/features/auth/ui/screens/register_screen.dart +git commit -m "refactor(auth): migrate hardcoded colors in register_screen" +``` + +--- + +### Task 11: 迁移其他页面(批量) + +**Files:** +- Modify: 所有包含硬编码颜色的页面文件 + +**Step 1: 逐页面迁移** + +按照频率顺序迁移: +1. `register_verification_screen.dart` +2. `settings_screen.dart` +3. `account_screen.dart` +4. `contacts_screen.dart` +5. `calendar_event_detail_screen.dart` +6. `add_contact_screen.dart` +7. `features_screen.dart` +8. `memory_screen.dart` +9. `home_screen.dart` +10. `todo_detail_screen.dart` + +每个文件重复 Task 10 的步骤。 + +**Step 2: 批量提交** + +```bash +git add apps/lib/features/ +git commit -m "refactor(ui): migrate hardcoded colors to AppColors" +``` + +--- + +### Task 12: 验证和测试 + +**Files:** +- None (validation only) + +**Step 1: 检查剩余硬编码** + +Run: `cd apps && grep -r "Color(0x" lib --include="*.dart" | wc -l` + +Expected: < 10 (只保留合理的动态颜色) + +**Step 2: 运行 Flutter 测试** + +Run: `cd apps && flutter test` + +Expected: All tests pass + +**Step 3: 视觉检查** + +手动运行应用,检查页面显示是否正常。 + +--- + +## Final Verification + +### Task 13: 全量测试 + +**Files:** +- None (testing only) + +**Step 1: 运行后端测试** + +Run: `cd backend && uv run pytest` + +Expected: All tests pass + +**Step 2: 运行 Flutter 测试** + +Run: `cd apps && flutter test` + +Expected: All tests pass + +**Step 3: 运行迁移检查** + +Run: `cd backend && uv run alembic check` + +Expected: No issues + +**Step 4: 类型检查** + +Run: `cd backend && uv run basedpyright src/` + +Expected: No errors + +--- + +## Summary + +**Total Tasks:** 13 + +**Estimated Time:** 3-4 hours + +**Key Deliverables:** +- ✅ user_agent_catalog.yaml 配置文件 +- ✅ UserAgentCatalog 模型 +- ✅ 数据库迁移(表 + trigger) +- ✅ 初始化函数扩展 +- ✅ 单元测试 +- ✅ 硬编码颜色迁移(109 → <10) + +**Success Criteria:** +- [ ] 新用户注册后自动创建 3 个 user_agents 记录 +- [ ] 已存在用户不受影响 +- [ ] 配置可通过 YAML + 重启更新 +- [ ] 硬编码颜色数量 < 10 +- [ ] 所有测试通过 +- [ ] UI 视觉无回归