Files
social-app/docs/plans/2026-03-02-bugfix-design.md
T

13 KiB
Raw Blame History

Bug Fix Design: TRIGGER-001 & TOKEN-001

Date: 2026-03-02 Author: AI Assistant Status: Approved

Overview

解决 docs/bugs/backlog.md 中的两个 bug

  1. TRIGGER-001: user_agents 自动创建
  2. TOKEN-001: Flutter 硬编码颜色迁移

TRIGGER-001: user_agents 自动创建

Problem

新用户注册时,user_agents 表未自动创建默认 Agent 配置记录,导致首次使用 Agent Chat 功能失败。

Solution

创建配置驱动的自动初始化机制:

  • YAML 配置文件定义默认 agent 配置
  • 数据库 catalog 表存储配置(持久化)
  • Trigger 从 catalog 表读取并批量插入
  • 应用启动时自动同步 YAML → catalog 表

Architecture

user_agent_catalog.yaml
  ↓ (应用启动时)
initialize_user_agent_catalog()
  ↓ (upsert)
user_agent_catalog 表
  ↓ (Trigger 查询)
新用户注册 → create_profile_for_new_user()
  ├─→ 插入 profiles
  └─→ 批量插入 user_agents (3条)

Data Model

1. user_agent_catalog.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

2. user_agent_catalog 表

CREATE TABLE user_agent_catalog (
    agent_type VARCHAR(20) PRIMARY KEY,
    llm_id UUID NOT NULL REFERENCES llms(id) ON DELETE RESTRICT,
    status VARCHAR(20) NOT NULL DEFAULT 'active',
    config JSONB NOT NULL DEFAULT '{}',
    created_at TIMESTAMPTZ DEFAULT now(),
    updated_at TIMESTAMPTZ DEFAULT now(),
    CONSTRAINT chk_status CHECK (status IN ('active', 'paused', 'migrating'))
);

字段说明:

  • agent_type: 主键,agent 类型(INTENT_RECOGNITION / TASK_EXECUTION / RESULT_REPORTING
  • llm_id: 外键,关联 llms 表
  • status: 默认状态(active / paused / migrating
  • config: 默认配置(JSONB,包含 temperature 等)

3. Trigger 函数

CREATE OR REPLACE FUNCTION create_profile_for_new_user()
RETURNS trigger AS $$
BEGIN
    -- 插入 profile(现有逻辑)
    INSERT INTO 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;

    -- 从 user_agent_catalog 批量插入 user_agents
    INSERT INTO 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 user_agent_catalog uac;
    
    RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public;

Implementation Components

1. Python Model

# backend/src/models/user_agent_catalog.py
class UserAgentCatalog(TimestampMixin, Base):
    __tablename__ = "user_agent_catalog"
    
    agent_type: Mapped[str] = mapped_column(String(20), primary_key=True)
    llm_id: Mapped[uuid.UUID] = mapped_column(UUID, ForeignKey("llms.id"), nullable=False)
    status: Mapped[str] = mapped_column(String(20), nullable=False)
    config: Mapped[dict] = mapped_column(JSONB, nullable=False, server_default="{}")

2. 初始化函数

# backend/src/core/config/initial/init_data.py

class UserAgentCatalogSeed(BaseModel):
    agent_type: str
    llm_model_code: str
    status: str
    config: dict[str, Any]

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 {}
    # ... validation logic
    return parsed.model_dump()

async def initialize_user_agent_catalog() -> None:
    catalog = load_user_agent_catalog()
    
    async with AsyncSessionLocal() as session:
        async with session.begin():
            for agent in catalog["agents"]:
                # 查找 llm_id
                llm = await session.execute(
                    select(Llm).where(Llm.model_code == agent["llm_model_code"])
                )
                llm_id = llm.scalar_one().id
                
                # Upsert
                existing = await session.execute(
                    select(UserAgentCatalog).where(
                        UserAgentCatalog.agent_type == agent["agent_type"]
                    )
                )
                catalog_entry = existing.scalar_one_or_none()
                
                if catalog_entry:
                    catalog_entry.llm_id = llm_id
                    catalog_entry.status = agent["status"]
                    catalog_entry.config = agent["config"]
                else:
                    session.add(UserAgentCatalog(
                        agent_type=agent["agent_type"],
                        llm_id=llm_id,
                        status=agent["status"],
                        config=agent["config"]
                    ))
    
    logger.info("Initialized user agent catalog")

async def initialize_data() -> bool:
    await initialize_llm_catalog()
    await initialize_user_agent_catalog()  # 新增
    return True

Migration Strategy

1. Alembic 迁移

# backend/alembic/versions/20260302_add_user_agent_catalog.py

def upgrade() -> None:
    # 创建表
    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(), nullable=False, server_default="{}"),
        sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()")),
        sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()")),
        sa.PrimaryKeyConstraint("agent_type"),
        sa.ForeignKeyConstraint(["llm_id"], ["llms.id"], ondelete="RESTRICT"),
    )
    
    op.execute(
        "ALTER TABLE user_agent_catalog "
        "ADD CONSTRAINT chk_status CHECK (status IN ('active', 'paused', 'migrating'))"
    )
    
    # 替换 trigger 函数
    op.execute("""
        CREATE OR REPLACE FUNCTION public.create_profile_for_new_user()
        -- ... 新的 trigger 代码
    """)
    
    # 为已存在用户补充 user_agents
    op.execute("""
        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
        );
    """)
    
    _enable_rls("user_agent_catalog")

def downgrade() -> None:
    # 恢复旧 trigger
    op.execute("""
        CREATE OR REPLACE FUNCTION public.create_profile_for_new_user()
        -- ... 旧的 trigger 代码(只有 profile
    """)
    
    _drop_rls("user_agent_catalog")
    op.drop_table("user_agent_catalog")

Configuration Update Flow

用户修改配置的步骤:

  1. 编辑 backend/src/core/config/static/database/user_agent_catalog.yaml
  2. 重启应用(或调用 uv run python -m core.runtime.cli bootstrap
  3. initialize_user_agent_catalog() 自动执行
  4. user_agent_catalog 表更新(upsert
  5. 新用户注册时使用新配置

示例:

# 修改 temperature
agents:
  - agent_type: INTENT_RECOGNITION
    llm_model_code: qwen3.5-flash
    status: active
    config:
      temperature: 0.5  # 从 0.7 改为 0.5

重启应用后,新注册的用户 INTENT_RECOGNITION agent 会使用 temperature=0.5。

Testing Strategy

  1. 单元测试: load_user_agent_catalog() 函数
  2. 集成测试: initialize_user_agent_catalog() upsert 逻辑
  3. E2E 测试: 新用户注册后自动创建 3 个 user_agents 记录
  4. 回归测试: 已存在用户不受影响

File Changes

backend/src/core/config/static/database/user_agent_catalog.yaml (填充)
backend/src/models/user_agent_catalog.py (新建)
backend/src/core/config/initial/init_data.py (扩展)
backend/alembic/versions/20260302_add_user_agent_catalog.py (新建)
backend/tests/unit/core/test_agent_init_data.py (扩展)

TOKEN-001: Flutter 硬编码颜色迁移

Problem

109 处硬编码 Color(0xFF...) 违反 apps/AGENTS.md 规则:"NEVER hardcode colors, sizes, or spacing values"。

分散在 11 个页面文件:

  • register_screen.dart, register_verification_screen.dart
  • settings_screen.dart, account_screen.dart
  • contacts_screen.dart, calendar_event_detail_screen.dart
  • add_contact_screen.dart, features_screen.dart
  • memory_screen.dart, home_screen.dart
  • todo_detail_screen.dart

Solution

渐进式迁移:

  1. 扫描所有硬编码颜色,统计频率
  2. 将高频颜色添加到 design_tokens.dartAppColors
  3. 逐页面替换硬编码为 AppColors.xxx
  4. 保留合理的动态颜色场景

Migration Steps

Phase 1: 扫描和统计

# 扫描所有硬编码颜色
grep -r "Color(0x" apps/lib --include="*.dart" | \
  sed 's/.*Color(0x\([0-9A-Fa-f]*\)).*/\1/' | \
  sort | uniq -c | sort -rn

预期输出:

  45 2196F3  # 蓝色(链接/主色)
  23 D32F2F  # 红色(错误)
  18 4CAF50  # 绿色(成功)
  ...

Phase 2: 扩展 AppColors

// apps/lib/core/theme/design_tokens.dart

class AppColors {
  // 现有颜色...
  
  // 从硬编码迁移的颜色
  static const Color linkBlue = Color(0xFF2196F3);
  static const Color errorRed = Color(0xFFD32F2F);
  static const Color successGreen = Color(0xFF4CAF50);
  // ... 根据扫描结果添加
}

Phase 3: 逐页面迁移

// Before
Container(
  color: Color(0xFF2196F3),
  child: Text('Link'),
)

// After
Container(
  color: AppColors.linkBlue,
  child: Text('Link'),
)

迁移顺序(按频率):

  1. register_screen.dart (最高频)
  2. register_verification_screen.dart
  3. settings_screen.dart
  4. ... (其他页面)

Phase 4: 测试和验证

  1. 视觉测试: 逐页面检查 UI 是否正常
  2. Widget 测试: 更新测试使用 AppColors
  3. 设计审查: 确认颜色一致性

Allowed Exceptions

以下场景可以保留硬编码(需在 AGENTS.md 中明确):

  • 动态计算的渐变色
  • 图片处理相关的颜色
  • 第三方库要求的颜色格式

Testing Strategy

  1. Widget 测试: 确保组件仍然正常渲染
  2. 视觉回归测试: 对比迁移前后的截图
  3. 代码审查: 确保没有遗漏的硬编码

File Changes

apps/lib/core/theme/design_tokens.dart (扩展)
apps/lib/features/*/ui/screens/*.dart (迁移)
apps/AGENTS.md (明确允许的场景)

Implementation Order

  1. TRIGGER-001 (优先级更高,影响功能)

    • 创建 catalog YAML
    • 创建 model 和迁移
    • 扩展初始化逻辑
    • 修改 trigger
    • 测试
  2. TOKEN-001 (次要,不影响功能)

    • 扫描硬编码
    • 扩展 AppColors
    • 逐页面迁移
    • 测试

Success Criteria

TRIGGER-001

  • 新用户注册后自动创建 3 个 user_agents 记录
  • 已存在用户不受影响
  • 配置可通过 YAML + 重启更新
  • 所有测试通过

TOKEN-001

  • 硬编码颜色数量从 109 减少到 < 10
  • UI 视觉无回归
  • 所有测试通过

Risks and Mitigations

TRIGGER-001

  • 风险: llms 表中缺少对应的 model_code
    • 缓解: 初始化前检查 llms 表是否存在,提供清晰的错误信息
  • 风险: Trigger 性能影响
    • 缓解: catalog 表只有 3 行,查询性能可忽略

TOKEN-001

  • 风险: 迁移后颜色不一致
    • 缓解: 视觉回归测试 + 设计审查
  • 风险: 遗漏某些硬编码
    • 缓解: 自动化扫描 + 代码审查
  • docs/bugs/backlog.md - Bug 列表
  • backend/AGENTS.md - Backend 开发规则
  • apps/AGENTS.md - Flutter 开发规则
  • backend/src/core/config/initial/init_data.py - 现有初始化逻辑
  • backend/alembic/versions/20260227_0006_invite_codes_and_profile_referral.py - Trigger 修改参考