Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions GROUPBRIEF_AI_IMAGE_STYLE_BATCH_TASK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# AI 图片风格中心、多群同步与当天筛选任务

## 背景

现有风格中心在桌面端占用过大并存在明显空白,触发器在无色板模式下文字粘连;AI 图片运行记录首次进入未默认筛选上海当天;群聊生图风格只能逐群保存。

## 目标

- 将共享 `ImageThemePicker` 改为桌面紧凑工作台(约 `960×720` 内)和 `390×844` 可用的移动全屏面板。
- AI 图片运行记录首次请求默认携带上海当天日期,清空日期后允许查看全部历史。
- 支持先确认一个风格,再搜索并勾选多个未删除群,通过单个批量接口同步配置。
- 部分失败时保留失败群并允许一键重试,不回滚已经成功的群。

## 允许修改范围

- `frontend/src/components/ImageThemePicker.tsx`
- `frontend/src/pages/v2/ai-images/**`
- `frontend/src/api.ts`
- `frontend/src/styles.css`
- 与上述交互直接相关的前端单测和 `frontend/e2e/ai-images.spec.ts`
- `app/api/groups.py`
- `app/db/repository.py`(仅新增窄范围的主题保存事务能力)
- 与批量群主题接口直接相关的后端测试
- 本执行说明文件

## 禁止修改范围

- 不重建或修改历史 Prompt、manifest、运行记录和历史图片。
- 不触发生图,不发送微信或邮件,不启动真实 Provider。
- 不改变群的 Prompt override、发送时间、发送开关或其他群配置。
- 不改变“每日随机”按群标识和日期独立随机的现有规则。
- 不连接生产数据库,不提交缓存、日志、构建产物或敏感配置。

## 已确定实现要求

### 风格中心

- 桌面端左侧四种模式导航,右侧显示设置与预览;移动端全屏、内容和操作区均可达。
- AI 自由发挥和每日随机显示效果、适用场景与随机规则。
- 预设风格提供搜索、分类、缩略图网格与紧凑详情。
- 自定义描述限制 80 字、拒绝空值和多行,保留计数与示例并缩短输入区。
- 底部取消/确认固定可见,保留草稿、Esc、焦点闭环和 reduced-motion。
- 触发器始终有模式图标或色板,标题/副标题分行,右侧显示“更换”和箭头;不依赖 `nth-child`。
- 只使用项目已声明的颜色变量,清理同区域无效旧 CSS。

### 多群同步

- 风格必须先在弹窗中明确确认;至少选择一个群后才能提交。
- 群列表支持搜索、复选框、全选当前列表、清空和已选数量。
- 每项显示群名、ID、当前风格及启用/停用状态;停用群仍允许预配置。
- 只发送一次批量请求;部分失败显示成功/失败清单,仅保留失败群为选中并切换为“重试失败群”。
- 多群场景展示“共享风格注入预览”,明确不会覆盖各群原有 Prompt 模板。

### API 与事务

- 新增 `PUT /api/groups/batch/image-theme`。
- 请求:`group_ids`、`image_theme`、`image_theme_custom`。
- 响应:`status`(`success|partial|failed`)、请求数量、成功群列表和带错误代码/可读原因的失败群列表。
- 主题键和自定义文本先做全局校验;非法主题、空自定义、超长或多行自定义返回 422 且零写入。
- 合法目标按请求顺序逐群使用独立事务;不存在、已删除或保存失败只记录该群失败,其他群继续。
- 只更新 `image_theme`、`image_theme_custom`、`updated_at`,不走会连带修改其他配置的通用更新路径。

## 验收标准

- 首次 AI 图片 runs 请求带上海当天日期;清空后请求不带日期。
- 无色板模式的触发器标题、副标题分行。
- 多选、全选、清空、单次批量请求、部分失败和只重试失败项有自动化覆盖。
- 批量接口覆盖全成功、部分成功、全局 422 零写入,并证明 Prompt override、发送配置和历史数据未变。
- 原有草稿、取消不应用、确认一次提交行为继续通过。
- 桌面与 `390×844` E2E 不溢出、列表可滚动、操作按钮可达。

## 测试命令

- 隔离 Python 环境运行相关后端测试及完整 `pytest` 两种顺序。
- `npm ci`
- `npm run build`
- `npm test`
- `npm run test:e2e`
- `python -m compileall -q app scripts tests`

## 返回格式

报告实际修改、测试结果、任务提交/远端 SHA、新 PR 地址和 CI 状态;不自动合并新 PR。
83 changes: 82 additions & 1 deletion app/api/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from __future__ import annotations

from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from pydantic import BaseModel, Field, field_validator
from sqlalchemy.exc import SQLAlchemyError
from sqlmodel import Session, select

from app.ai.image_themes import (
Expand Down Expand Up @@ -98,6 +99,21 @@ class GroupImagePromptUpdate(BaseModel):
expected_revision: str = ""


class BatchImageThemeUpdate(BaseModel):
group_ids: list[int] = Field(min_length=1)
image_theme: str
image_theme_custom: str = ""

@field_validator("group_ids")
@classmethod
def validate_group_ids(cls, values: list[int]) -> list[int]:
if any(group_id <= 0 for group_id in values):
raise ValueError("群 ID 必须是正整数")
if len(set(values)) != len(values):
raise ValueError("群 ID 不能重复")
return values


def _validate_group_theme(theme: object, custom: object = "") -> tuple[str, str]:
"""将主题配置统一交给后端目录校验,并转换成明确的 422。"""
try:
Expand Down Expand Up @@ -274,6 +290,71 @@ def create_group(
return {"id": group.id, "restored": False}


@router.put("/batch/image-theme")
def batch_update_group_image_theme(
payload: BatchImageThemeUpdate,
session: Session = Depends(repo.get_session),
):
"""按请求顺序独立保存群主题;单群失败不回滚已经成功的群。"""

theme, custom = _validate_group_theme(payload.image_theme, payload.image_theme_custom)
successes: list[dict] = []
failures: list[dict] = []

for group_id in payload.group_ids:
group = repo.get_group(session, group_id)
if group is None:
failures.append({
"group_id": group_id,
"code": "GROUP_NOT_FOUND",
"reason": "群不存在",
})
continue
if group.deleted_at is not None:
failures.append({
"group_id": group_id,
"code": "GROUP_DELETED",
"reason": "群已移入回收站",
})
continue

group_name = group.display_name or group.wechat_group_name or f"群 {group_id}"
try:
updated = repo.update_group_image_theme(
session,
group_id,
image_theme=theme,
image_theme_custom=custom,
)
except SQLAlchemyError:
session.rollback()
failures.append({
"group_id": group_id,
"code": "DATABASE_SAVE_FAILED",
"reason": "数据库保存失败,请重试",
})
continue
if not updated:
failures.append({
"group_id": group_id,
"code": "GROUP_DELETED",
"reason": "群已不存在或移入回收站",
})
continue
successes.append({
"group_id": group_id,
"group_name": group_name,
})

status = "success" if not failures else "partial" if successes else "failed"
return {
"status": status,
"requested_count": len(payload.group_ids),
"success": successes,
"failed": failures,
}


@router.put("/{group_id}")
def update_group(
group_id: int,
Expand Down
24 changes: 23 additions & 1 deletion app/db/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from pathlib import Path
from typing import Any

from sqlalchemy import event
from sqlalchemy import event, update
from sqlmodel import Session, SQLModel, create_engine, select

from app.config.settings import Settings
Expand Down Expand Up @@ -523,6 +523,28 @@ def save_group(session: Session, group: Group) -> Group:
return group


def update_group_image_theme(
session: Session,
group_id: int,
*,
image_theme: str,
image_theme_custom: str,
) -> bool:
"""独立事务只更新群生图主题字段,避免通用保存路径带入其他配置。"""

result = session.execute(
update(Group)
.where(Group.id == group_id, Group.deleted_at.is_(None))
.values(
image_theme=image_theme,
image_theme_custom=image_theme_custom,
updated_at=_now(),
)
)
session.commit()
return result.rowcount == 1


def delete_group(session: Session, group_id: int) -> Group | None:
group = session.get(Group, group_id)
if group is None:
Expand Down
Loading