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

459 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 修改参考