Files
social-app/docs/plans/2026-03-02-bugfix-plan.md
T
2026-03-02 15:19:22 +08:00

20 KiB
Raw Blame History

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 配置

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

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: 创建模型文件

# 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 中导出

# 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

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: 编写迁移逻辑

# 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

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 模型

# 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 函数

# 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: 添加初始化函数

# 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 函数

# 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

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: 添加测试

# 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

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(手动)

-- 创建测试用户
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: 执行补充脚本

-- 为已存在但没有 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:

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: 添加新颜色常量

// 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

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: 替换硬编码颜色

// 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

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: 批量提交

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 视觉无回归