diff --git a/docs/plans/2026-03-02-bugfix-design.md b/docs/plans/2026-03-02-bugfix-design.md new file mode 100644 index 0000000..a7c493d --- /dev/null +++ b/docs/plans/2026-03-02-bugfix-design.md @@ -0,0 +1,458 @@ +# 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 修改参考