# 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 ```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 表 ```sql 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 函数 ```sql 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 ```python # 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. 初始化函数 ```python # 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 迁移 ```python # 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. 新用户注册时使用新配置 **示例:** ```yaml # 修改 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.dart` 的 `AppColors` 3. 逐页面替换硬编码为 `AppColors.xxx` 4. 保留合理的动态颜色场景 ### Migration Steps #### Phase 1: 扫描和统计 ```bash # 扫描所有硬编码颜色 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 ```dart // 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: 逐页面迁移 ```dart // 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 - **风险:** 迁移后颜色不一致 - **缓解:** 视觉回归测试 + 设计审查 - **风险:** 遗漏某些硬编码 - **缓解:** 自动化扫描 + 代码审查 ## Related Documents - `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 修改参考