You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

7.0 KiB

AI 对话消息 Schema camelCase 规范修复

变更信息

  • 日期:2026-02-06
  • 类型:Bug 修复 / 规范统一
  • 影响范围:AI 对话消息 API 响应格式
  • Breaking Change:否(通过 populate_by_name 兼容)

问题描述

ai_conversation_message.py 的响应 Schema 字段命名不符合项目规范:

  • 使用 snake_case(如 message_id, meta_data, ai_job_id
  • 应该使用 camelCase(如 messageId, metaData, aiJobId

参考 screenplay.py 实现,API 响应字段应使用 camelCase 以符合前端约定。

解决方案

修改内容

为所有响应 Schema 添加 camelCase alias:

1. AIConversationMessageResponse

class AIConversationMessageResponse(BaseModel):
    """消息响应"""
    message_id: UUID = Field(..., alias="messageId", description="消息 ID")
    conversation_id: UUID = Field(..., alias="conversationId", description="对话会话 ID")
    user_id: UUID = Field(..., alias="userId", description="用户 ID")
    ai_job_id: Optional[UUID] = Field(None, alias="aiJobId", description="关联的 AI 任务 ID")
    # ... 其他字段
    meta_data: Dict[str, Any] = Field(default_factory=dict, alias="metaData", description="额外元数据")
    order_index: int = Field(..., alias="orderIndex", description="消息顺序")
    created_at: datetime = Field(..., alias="createdAt", description="创建时间")
    updated_at: datetime = Field(..., alias="updatedAt", description="更新时间")
    
    model_config = ConfigDict(from_attributes=True, populate_by_name=True)

2. AIConversationMessageListItem

class AIConversationMessageListItem(BaseModel):
    """消息列表项"""
    message_id: UUID = Field(..., alias="messageId", description="消息 ID")
    # ... 其他字段
    meta_data: Dict[str, Any] = Field(default_factory=dict, alias="metaData", description="额外元数据")
    ai_job_id: Optional[UUID] = Field(None, alias="aiJobId", description="关联的 AI 任务 ID")
    created_at: datetime = Field(..., alias="createdAt", description="创建时间")
    
    model_config = ConfigDict(from_attributes=True, populate_by_name=True)

3. AIConversationMessageListResponse

class AIConversationMessageListResponse(BaseModel):
    """消息列表响应"""
    items: list[AIConversationMessageListItem] = Field(..., description="消息列表")
    total: int = Field(..., description="总数")
    page: int = Field(..., description="当前页码")
    page_size: int = Field(..., alias="pageSize", description="每页数量")
    total_pages: int = Field(..., alias="totalPages", description="总页数")
    
    model_config = ConfigDict(populate_by_name=True)

4. TriggerAIGenerationResponse

class TriggerAIGenerationResponse(BaseModel):
    """触发 AI 生成响应"""
    job_id: UUID = Field(..., alias="jobId", description="任务 ID")
    task_id: str = Field(..., alias="taskId", description="Celery 任务 ID")
    status: str = Field(..., description="任务状态")
    estimated_credits: int = Field(..., alias="estimatedCredits", description="预估积分消耗")
    reference_images_count: int = Field(0, alias="referenceImagesCount", description="参考图数量")
    
    model_config = ConfigDict(populate_by_name=True)

关键配置

ConfigDict 参数说明

  • from_attributes=True:支持从 ORM 模型创建 Pydantic 实例
  • populate_by_name=True:支持同时使用字段名和 alias(向后兼容)

字段映射表

Python 字段名 JSON alias (API) 说明
message_id messageId 消息 ID
conversation_id conversationId 对话会话 ID
user_id userId 用户 ID
ai_job_id aiJobId AI 任务 ID
meta_data metaData 元数据
order_index orderIndex 排序索引
created_at createdAt 创建时间
updated_at updatedAt 更新时间
page_size pageSize 每页数量
total_pages totalPages 总页数
job_id jobId 任务 ID
task_id taskId Celery 任务 ID
estimated_credits estimatedCredits 预估积分
reference_images_count referenceImagesCount 参考图数量

测试验证

单元测试

docker exec jointo-server-app pytest tests/unit/repositories/test_ai_conversation_message_repository.py -v

结果 17 passed

集成测试

docker exec jointo-server-app pytest tests/integration/test_ai_conversation_api.py -v

结果 23 passed

完整测试套件

docker exec jointo-server-app pytest \
  tests/unit/repositories/test_ai_conversation_message_repository.py \
  tests/integration/test_ai_conversation_api.py -v

结果 40 passed

API 响应示例

发送消息响应(修复后)

{
  "code": 200,
  "message": "Success",
  "data": {
    "messageId": "019c3163-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "conversationId": "019c3163-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "userId": "019c3163-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "aiJobId": null,
    "role": 1,
    "content": "这是一条测试消息",
    "metaData": {
      "custom_field": "custom_value"
    },
    "orderIndex": 1,
    "createdAt": "2026-02-06T05:18:45.123456Z",
    "updatedAt": "2026-02-06T05:18:45.123456Z"
  }
}

消息列表响应(修复后)

{
  "code": 200,
  "message": "Success",
  "data": {
    "items": [
      {
        "messageId": "019c3163-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "role": 1,
        "content": "测试消息",
        "metaData": {},
        "aiJobId": null,
        "createdAt": "2026-02-06T05:18:45.123456Z"
      }
    ],
    "total": 1,
    "page": 1,
    "pageSize": 50,
    "totalPages": 1
  }
}

向后兼容性

通过 populate_by_name=True 配置,API 同时支持:

  • camelCase(推荐):{"messageId": "xxx"}
  • snake_case(兼容):{"message_id": "xxx"}

注意:响应始终使用 camelCase,但接受两种命名风格的输入。

影响分析

前端影响

  • 无影响:前端已经在使用 camelCase 字段名
  • 集成测试已验证响应格式正确

后端影响

  • 无影响:内部代码仍使用 snake_case
  • Pydantic 自动处理字段名转换

参考规范

参考 server/app/schemas/screenplay.py 的实现:

  • 所有响应 Schema 使用 alias 定义 camelCase 字段名
  • 使用 ConfigDict(populate_by_name=True) 支持向后兼容
  • 保持内部代码使用 snake_case(符合 Python PEP 8)

相关文档

总结

修复完成

  • 所有响应 Schema 已添加 camelCase alias
  • 通过 40 个测试(17 单元 + 23 集成)
  • 保持向后兼容性
  • 符合项目技术栈规范