459 lines
13 KiB
Markdown
459 lines
13 KiB
Markdown
|
|
# 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 修改参考
|