Y 3 ヶ月 前
コミット
c9e7537270
100 ファイル変更4461 行追加0 行削除
  1. 11 0
      .gitignore
  2. BIN
      002.jpg
  3. BIN
      003.jpg
  4. 194 0
      ASYNC_SYSTEM_GUIDE.md
  5. 6 0
      TODO.MD
  6. BIN
      ai_swap.db
  7. 44 0
      backend/.dockerignore
  8. 3 0
      backend/.env
  9. 35 0
      backend/Dockerfile
  10. 92 0
      backend/api/ai_gen_video_api.py
  11. 841 0
      backend/api/ai_swap_api.py
  12. 116 0
      backend/api/ai_swap_bg_api.py
  13. 107 0
      backend/api/ai_swap_cloth_api.py
  14. 107 0
      backend/api/ai_swap_face_api.py
  15. 64 0
      backend/api/auth_api.py
  16. 392 0
      backend/api/auto_post_api.py
  17. 79 0
      backend/api/user_material_api.py
  18. 98 0
      backend/api/user_template_api.py
  19. 3 0
      backend/config/ai_gen_video.json
  20. 5 0
      backend/config/ai_swap_bg_config.json
  21. 5 0
      backend/config/ai_swap_cloth_config.json
  22. 5 0
      backend/config/ai_swap_face_config.json
  23. 65 0
      backend/config/prompt_config.py
  24. 5 0
      backend/config/workflow_config.json
  25. BIN
      backend/db/ai_swap.db
  26. 242 0
      backend/examples/ai_swap_example.py
  27. 36 0
      backend/examples/auth_example.py
  28. 35 0
      backend/examples/user_material_example.py
  29. 0 0
      backend/modules/__init__.py
  30. 0 0
      backend/modules/auto_post/__init__.py
  31. 192 0
      backend/modules/auto_post/base_page.py
  32. 149 0
      backend/modules/auto_post/post.py
  33. 42 0
      backend/modules/auto_post/post_config.py
  34. 360 0
      backend/modules/auto_post/post_page.py
  35. BIN
      backend/modules/auto_post/temp/flower.jpg
  36. BIN
      backend/modules/auto_post/temp/test_img.png
  37. 1 0
      backend/modules/auto_post/user_data/AmountExtractionHeuristicRegexes/3/_metadata/verified_contents.json
  38. 3 0
      backend/modules/auto_post/user_data/AmountExtractionHeuristicRegexes/3/heuristic_regexes.binarypb
  39. 5 0
      backend/modules/auto_post/user_data/AmountExtractionHeuristicRegexes/3/manifest.json
  40. 10 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/AD
  41. 10 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/AE
  42. 44 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/AF
  43. 13 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/AG
  44. 14 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/AL
  45. 15 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/AM
  46. 21 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/AO
  47. 29 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/AR
  48. 7 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/AS
  49. 14 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/AT
  50. 12 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/AU
  51. 5 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/AX
  52. 4 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/AZ
  53. 5 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BA
  54. 15 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BB
  55. 10 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BD
  56. 6 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BE
  57. 18 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BF
  58. 37 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BG
  59. 6 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BH
  60. 20 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BI
  61. 15 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BJ
  62. 13 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BM
  63. 6 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BN
  64. 14 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BO
  65. 5 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BQ
  66. 31 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BR
  67. 35 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BS
  68. 24 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BT
  69. 21 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BW
  70. 8 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BY
  71. 8 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BZ
  72. 15 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CA
  73. 3 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CC
  74. 32 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CD
  75. 22 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CF
  76. 16 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CG
  77. 33 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CH
  78. 18 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CI
  79. 29 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CL
  80. 12 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CM
  81. 33 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CN
  82. 42 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CO
  83. 11 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CR
  84. 20 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CU
  85. 28 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CV
  86. 3 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CX
  87. 9 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CY
  88. 16 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CZ
  89. 19 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/DE
  90. 11 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/DJ
  91. 10 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/DK
  92. 16 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/DM
  93. 36 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/DO
  94. 69 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/DZ
  95. 31 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/EC
  96. 17 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/EE
  97. 35 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/EG
  98. 5 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/EH
  99. 11 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/ER
  100. 22 0
      backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/ES

+ 11 - 0
.gitignore

@@ -0,0 +1,11 @@
+__pycache__/
+.pytest_cache/
+output/
+cache/
+data/
+node_modules/
+frontend/
+logs/
+.bin/
+.cache/
+*.pyc

BIN
002.jpg


BIN
003.jpg


+ 194 - 0
ASYNC_SYSTEM_GUIDE.md

@@ -0,0 +1,194 @@
+# AI换脸换装异步任务系统使用指南
+
+## 🚀 系统概述
+
+本系统实现了真正的异步任务处理,确保在执行AI换脸换装任务时,用户可以正常访问其他页面功能(如历史记录、素材库等),无需等待AI任务完成。
+
+## 🏗️ 技术架构
+
+### 后端架构
+- **任务队列服务**: 基于线程池的异步任务处理
+- **FastAPI**: 异步Web框架,支持高并发
+- **多线程处理**: AI任务在独立线程中执行
+- **状态管理**: 实时任务状态跟踪
+
+### 前端架构
+- **React**: 组件化UI,独立状态管理
+- **轮询机制**: 定期检查任务状态
+- **实时反馈**: 任务进度和状态显示
+
+## 📋 功能特性
+
+### ✅ 核心功能
+1. **非阻塞任务提交**: 立即返回任务ID,不阻塞用户界面
+2. **实时状态监控**: 轮询任务状态,实时显示进度
+3. **并发API访问**: 其他页面功能正常使用
+4. **任务管理**: 支持取消、查看任务列表
+5. **错误处理**: 完善的异常处理和用户反馈
+
+### ✅ 用户体验
+1. **即时响应**: 任务提交后立即可以切换页面
+2. **状态透明**: 清楚显示任务状态和进度
+3. **操作灵活**: 支持取消正在进行的任务
+4. **历史记录**: 任务完成后自动更新历史记录
+
+## 🔧 使用方法
+
+### 1. 启动系统
+```bash
+# 启动后端服务
+cd backend
+python -m uvicorn api.ai_swap_api:app --reload --host 0.0.0.0 --port 8000
+
+# 启动前端服务
+cd new_frontend
+npm start
+```
+
+### 2. 使用流程
+1. **登录系统**: 访问登录页面,输入用户凭据
+2. **上传素材**: 在素材库页面上传人脸和服装素材
+3. **开始处理**: 在AI换脸换装页面选择素材并输入提示词
+4. **任务提交**: 点击"开始AI换脸换装"按钮
+5. **状态监控**: 查看任务状态和进度
+6. **自由操作**: 可以切换到其他页面,功能正常使用
+7. **结果查看**: 任务完成后查看处理结果
+
+### 3. 任务状态说明
+- **等待中 (pending)**: 任务已提交,等待处理
+- **处理中 (processing)**: 任务正在执行
+- **已完成 (completed)**: 任务成功完成
+- **失败 (failed)**: 任务执行失败
+- **已取消 (cancelled)**: 任务被用户取消
+
+## 🧪 测试验证
+
+### 1. 运行测试脚本
+```bash
+python test_async_system.py
+```
+
+### 2. 手动测试步骤
+1. 启动AI换脸换装任务
+2. 立即切换到历史记录页面
+3. 验证历史记录正常加载
+4. 切换到素材库页面
+5. 验证素材列表正常显示
+6. 返回AI换脸换装页面查看任务状态
+
+### 3. 预期结果
+- ✅ 其他页面可以正常访问和加载数据
+- ✅ 任务状态实时更新
+- ✅ 系统响应流畅,无阻塞现象
+- ✅ 任务完成后结果正常显示
+
+## 📊 API接口
+
+### 任务管理接口
+```http
+# 提交AI换脸换装任务
+POST /api/v1/swap
+{
+  "user_id": 3,
+  "face_image_id": 21,
+  "cloth_image_id": 40,
+  "prompt": "创意提示词"
+}
+
+# 获取任务状态
+GET /api/v1/tasks/{task_id}
+
+# 获取用户任务列表
+GET /api/v1/users/{user_id}/tasks
+
+# 取消任务
+DELETE /api/v1/tasks/{task_id}
+
+# 获取队列统计
+GET /api/v1/tasks/queue/stats
+```
+
+### 响应示例
+```json
+{
+  "success": true,
+  "task_id": "550e8400-e29b-41d4-a716-446655440000",
+  "process_record_id": null,
+  "result_image_id": null,
+  "copywriter_text": null,
+  "history_prompt": null
+}
+```
+
+## 🔍 监控和调试
+
+### 1. 后端日志
+```bash
+# 查看任务队列日志
+tail -f backend/logs/ai_swap_service.log
+
+# 查看API访问日志
+tail -f backend/logs/ai_swap_api.log
+```
+
+### 2. 前端调试
+```javascript
+// 浏览器控制台查看任务状态
+console.log('当前任务ID:', currentTaskId);
+console.log('任务状态:', taskStatus);
+
+// 网络面板查看API请求
+// 检查 /api/v1/tasks/{task_id} 请求
+```
+
+### 3. 队列监控
+```bash
+# 查看队列统计
+curl http://localhost:8000/api/v1/tasks/queue/stats
+```
+
+## ⚠️ 注意事项
+
+### 1. 系统要求
+- Python 3.8+
+- Node.js 14+
+- 足够的内存和CPU资源
+- 稳定的网络连接
+
+### 2. 性能考虑
+- 任务队列最大并发数: 3个
+- 轮询间隔: 2秒
+- 任务超时: 无限制(建议监控长时间任务)
+
+### 3. 错误处理
+- 网络异常: 自动重试轮询
+- 任务失败: 显示错误信息
+- 系统异常: 记录详细日志
+
+## 🚀 扩展功能
+
+### 1. 可扩展功能
+- WebSocket实时推送
+- 任务优先级管理
+- 分布式任务队列
+- 任务结果缓存
+- 批量任务处理
+
+### 2. 性能优化
+- 数据库连接池优化
+- 静态文件CDN加速
+- 图片压缩和缓存
+- API响应缓存
+
+## 📞 技术支持
+
+如遇到问题,请检查:
+1. 后端服务是否正常启动
+2. 任务队列服务是否运行
+3. 数据库连接是否正常
+4. 网络连接是否稳定
+5. 浏览器控制台是否有错误
+
+---
+
+**总结**: 本异步任务系统确保了在执行AI换脸换装任务时,用户可以正常使用其他页面功能,提供了良好的用户体验和系统响应性。 

+ 6 - 0
TODO.MD

@@ -0,0 +1,6 @@
+## 新增生成结果图
+1、process_records新增字段:task_type,prompt(√)
+2、数据库迁移(√)
+3、数据库操作类升级(√)
+4、生图接口修改:保存记录需保存新增任务类型、提示词字段(√)
+5、

BIN
ai_swap.db


+ 44 - 0
backend/.dockerignore

@@ -0,0 +1,44 @@
+# Python caches
+__pycache__
+*.pyc
+*.pyo
+*.pyd
+
+# VCS
+.git
+.gitignore
+
+# Virtual envs
+.venv
+venv
+env
+
+# Byte-compiled / optimized / DLL files
+.mypy_cache
+.pytest_cache
+.ruff_cache
+
+# OS files
+.DS_Store
+Thumbs.db
+
+# Build artifacts
+build
+dist
+*.egg-info
+
+# Data/logs/outputs
+logs
+output
+data
+
+# Local configs
+.env
+*.local
+*.secret
+*.secrets
+*.bak
+
+# IDE
+.vscode
+.idea

+ 3 - 0
backend/.env

@@ -0,0 +1,3 @@
+FAL_KEY="70706222-2030-49a5-8f28-986b67d99636:e3abcff573c2d65b879e15d992c9ecb1"
+
+FAL_KEY2="2ebba018-d403-4a38-a894-48478faedece:7d8d723ce7abf8c375a80aef86a4ae4d"

+ 35 - 0
backend/Dockerfile

@@ -0,0 +1,35 @@
+# syntax=docker/dockerfile:1
+
+FROM python:3.10-slim AS base
+
+ENV PYTHONDONTWRITEBYTECODE=1 \
+    PYTHONUNBUFFERED=1 \
+    PIP_NO_CACHE_DIR=1 \
+    PIP_DISABLE_PIP_VERSION_CHECK=1 \
+    UVICORN_WORKERS=2 \
+    PORT=8002
+
+WORKDIR /app
+
+# 安装系统依赖(如需 Pillow 等编译依赖,可按需添加)
+RUN apt-get update \
+    && apt-get install -y --no-install-recommends build-essential \
+    && rm -rf /var/lib/apt/lists/*
+
+# 仅拷贝依赖文件,加速构建缓存
+COPY requirements.txt /app/backend/requirements.txt
+RUN pip install -r /app/backend/requirements.txt
+
+# 拷贝后端代码
+COPY . /app/backend
+
+# 创建非 root 用户(更安全)
+RUN useradd -m appuser && chown -R appuser:appuser /app
+USER appuser
+
+EXPOSE 8000
+
+# 生产环境不需要 reload
+CMD ["sh", "-c", "uvicorn backend.api.ai_swap_api:app --host 0.0.0.0 --port ${PORT} --workers ${UVICORN_WORKERS}"]
+
+

+ 92 - 0
backend/api/ai_gen_video_api.py

@@ -0,0 +1,92 @@
+"""
+AI图生视频API接口
+提供RESTful API接口,支持图生视频、历史记录查询等功能
+"""
+
+from fastapi import HTTPException, APIRouter
+from pydantic import BaseModel, Field
+from typing import Optional
+
+from backend.services.ai_gen_video_service import AIGenVideoService
+from backend.modules.database.operations import DatabaseOperations
+from backend.utils.logger_config import setup_logger
+from backend.services.task_queue_service import get_task_queue_service
+
+logger = setup_logger(__name__)
+
+router = APIRouter()
+
+# 创建服务实例
+ai_gen_video_service = AIGenVideoService()
+db_ops = DatabaseOperations()
+
+# 启动任务队列服务
+task_queue_service = get_task_queue_service()
+task_queue_service.start()
+
+# ==================== 数据模型 ====================
+
+class GenVideoRequest(BaseModel):
+    user_id: int = Field(..., description="用户ID")
+    image_id: int = Field(..., description="图片ID")
+    prompt:  str = Field(..., description="提示词", max_length=500)
+    quantity: Optional[int] = Field(1, ge=1, le=10, description="每组生成数量")
+
+class GenVideoResponse(BaseModel):
+    success: bool
+    task_id: Optional[str] = None
+    process_record_id: Optional[int] = None
+    result_image_id: Optional[int] = None
+    copywriter_text: Optional[str] = None
+    error: Optional[str] = None
+    error_type: Optional[str] = None
+
+# ==================== 中间件和依赖 ====================
+
+async def verify_user(user_id: int):
+    """验证用户是否存在且有效"""
+    user = db_ops.get_user_by_id(user_id)
+    if not user:
+        raise HTTPException(status_code=404, detail=f"用户ID {user_id} 不存在")
+    if not user.get("is_active", False):
+        raise HTTPException(status_code=403, detail="用户账户已被禁用")
+    return user
+
+# ==================== API路由 ====================
+@router.post("/api/v1/i2v", response_model=GenVideoResponse, tags=["AI图生视频"])
+async def process_gen_video(request: GenVideoRequest):
+    """
+    执行AI图生视频(异步任务)
+    
+    - **user_id**: 用户ID
+    - **image_id**: 图片ID
+    - **prompt**: 提示词
+    - **quantity**: 每组生成数量(可选)
+    """
+    try:
+        # 验证用户
+        await verify_user(request.user_id)
+
+        # 提交任务到队列(非阻塞)
+        task_id = ai_gen_video_service.submit_gen_video_task(
+            user_id=request.user_id,
+            image_id=request.image_id,
+            prompt=request.prompt,
+            quantity=request.quantity or 1
+        )
+
+        logger.info(f"用户 {request.user_id} 图生视频任务已提交,任务ID: {task_id}")
+        return GenVideoResponse(
+            success=True,
+            task_id=task_id,
+            process_record_id=None,
+            result_image_id=None,
+            copywriter_text=None
+        )
+
+    except HTTPException:
+        raise
+    except Exception as e:
+        logger.error(f"API处理异常: {str(e)}", exc_info=True)
+        raise HTTPException(status_code=500, detail=f"服务器内部错误: {str(e)}")
+    

+ 841 - 0
backend/api/ai_swap_api.py

@@ -0,0 +1,841 @@
+"""
+AI换脸换装API接口
+提供RESTful API接口,支持换脸换装、历史记录查询等功能
+"""
+
+from fastapi import FastAPI, HTTPException, Depends, UploadFile, File, Form, Body
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import JSONResponse
+from starlette.responses import FileResponse
+from fastapi.staticfiles import StaticFiles
+import os
+from pydantic import BaseModel, Field
+from typing import Optional, List, Dict, Any, Union
+import uvicorn
+from datetime import datetime, timedelta
+from pydantic import BaseModel
+from sqlalchemy import func, and_
+
+from backend.services.ai_swap_service import AISwapService, process_swap_with_record
+from backend.modules.database.operations import DatabaseOperations
+from backend.utils.logger_config import setup_logger
+from backend.api.auth_api import router as auth_router
+from backend.api.user_material_api import router as user_material_router
+from backend.api.user_template_api import router as user_template_router
+from backend.modules.database.models import ProcessRecord
+from backend.api.auto_post_api import router as auto_post_router
+from backend.api.ai_swap_bg_api import router as ai_swap_bg_router
+from backend.api.ai_swap_cloth_api import router as ai_swap_cloth_router
+from backend.api.ai_swap_face_api import router as ai_swap_face_router
+from backend.api.ai_gen_video_api import router as ai_gen_video_router
+
+logger = setup_logger(__name__)
+
+# 创建FastAPI应用
+app = FastAPI(
+    title="AI换脸换装服务",
+    description="包含用户认证与管理、AI换脸换装等API",
+    version="1.0.0"
+)
+
+# 挂载静态目录:使用与当前文件相对的绝对路径,避免工作目录变化导致路径失效
+current_dir = os.path.dirname(os.path.abspath(__file__))
+project_root = os.path.dirname(os.path.dirname(current_dir))  
+
+materials_dir = os.path.abspath(os.path.join(project_root, 'backend', 'data', 'materials'))
+output_dir = os.path.abspath(os.path.join(project_root, 'backend', 'output'))
+
+# 确保目录存在(StaticFiles 需要已存在的目录)
+os.makedirs(materials_dir, exist_ok=True)
+os.makedirs(output_dir, exist_ok=True)
+
+allowed_origins = [
+    "http://localhost:5173",
+    "http://127.0.0.1:5173",
+    "http://localhost:3000",
+    "http://127.0.0.1:3000",
+    "http://10.41.175.254:5173",
+    "http://10.41.175.254:3000",
+    # 局域网访问(当前机器IP)
+    "http://10.41.175.43:5173",
+    "http://10.41.175.43:3000",
+    # 局域网访问(域名访问)
+    "http://ai-swap.local:5173",
+]
+
+# 为静态资源单独包裹 CORS 中间件,确保跨域下载(fetch blob)也包含 CORS 头
+materials_app = StaticFiles(directory=materials_dir)
+materials_app = CORSMiddleware(
+    app=materials_app,
+    allow_origins=["*"],  # 静态资源放宽到任意来源,避免浏览器下载报CORS
+    allow_credentials=False,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+app.mount("/materials", materials_app, name="materials")
+
+output_app = StaticFiles(directory=output_dir)
+output_app = CORSMiddleware(
+    app=output_app,
+    allow_origins=["*"],  # 静态资源放宽到任意来源,避免浏览器下载报CORS
+    allow_credentials=False,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+app.mount("/output", output_app, name="output")
+
+# 添加CORS中间件
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=allowed_origins,
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+
+# 注册认证API
+app.include_router(auth_router, prefix="/auth", tags=["auth"])
+app.include_router(user_material_router)
+app.include_router(auto_post_router, prefix="/auto_post")
+app.include_router(user_template_router)
+app.include_router(ai_swap_bg_router)
+app.include_router(ai_swap_cloth_router)
+app.include_router(ai_swap_face_router)
+app.include_router(ai_gen_video_router)
+
+# 创建服务实例
+ai_swap_service = AISwapService()
+db_ops = DatabaseOperations()
+
+# 启动任务队列服务
+from backend.services.task_queue_service import get_task_queue_service
+task_queue_service = get_task_queue_service()
+task_queue_service.start()
+
+
+# ==================== 数据模型 ====================
+
+class SwapRequest(BaseModel):
+    """换脸换装请求模型"""
+    user_id: int = Field(..., description="用户ID")
+    face_image_id: int = Field(..., description="人脸图片ID")
+    cloth_image_id: int = Field(..., description="服装图片ID")
+    prompt: str = Field(..., description="提示词", max_length=500)
+    face_prompt: Optional[str] = Field(None, description="人脸识别提示词")
+    cloth_prompt: Optional[str] = Field(None, description="服装识别提示词")
+    prompt_prompt: Optional[str] = Field(None, description="提示词优化")
+    quantity: Optional[int] = Field(1, ge=1, le=10, description="每组生成数量")
+
+
+class SwapResponse(BaseModel):
+    """换脸换装响应模型"""
+    success: bool
+    task_id: Optional[str] = None
+    process_record_id: Optional[int] = None
+    result_image_id: Optional[int] = None
+    copywriter_text: Optional[str] = None
+    history_prompt: Optional[str] = None
+    error: Optional[str] = None
+    error_type: Optional[str] = None
+
+
+class TaskStatusResponse(BaseModel):
+    """任务状态响应模型"""
+    task_id: str
+    status: str
+    progress: int
+    created_at: str
+    started_at: Optional[str] = None
+    completed_at: Optional[str] = None
+    result: Optional[Dict[str, Any]] = None
+    error: Optional[str] = None
+
+
+class UserTasksResponse(BaseModel):
+    """用户任务列表响应模型"""
+    tasks: List[Dict[str, Any]]
+    total: int
+
+
+class ProcessHistoryResponse(BaseModel):
+    """处理历史响应模型"""
+    records: List[Dict[str, Any]]
+    total: int
+    page: int
+    page_size: int
+    total_pages: int
+
+
+class ProcessDetailResponse(BaseModel):
+    """处理详情响应模型"""
+    process_record: Dict[str, Any]
+    face_image: Optional[Dict[str, Any]] = None
+    cloth_image: Optional[Dict[str, Any]] = None
+    result_image: Optional[Dict[str, Any]] = None
+
+
+class UpdateTextRequest(BaseModel):
+    """更新文本请求模型"""
+    user_id: int = Field(..., description="用户ID")
+    title: str = Field(..., description="标题")
+    content: str = Field(..., description="文案内容")
+    label: str = Field(..., description="话题标签")
+
+    class Config:
+        json_schema_extra = {
+            "example": {
+                "user_id": 1,
+                "title": "示例标题",
+                "content": "示例文案内容",
+                "label": "示例标签"
+            }
+        }
+
+
+class ApproveRecordRequest(BaseModel):
+    """审核记录请求模型"""
+    user_id: int = Field(..., description="用户ID")
+    
+    class Config:
+        json_schema_extra = {
+            "example": {
+                "user_id": 1
+            }
+        }
+
+
+class ApproveRecordResponse(BaseModel):
+    """审核记录响应模型"""
+    success: bool
+    message: str
+    process_record: Optional[Dict[str, Any]] = None
+
+
+class DeleteResultImageRequest(BaseModel):
+    """删除结果图片请求模型"""
+    user_id: int = Field(..., description="用户ID")
+    
+    class Config:
+        json_schema_extra = {
+            "example": {
+                "user_id": 1
+            }
+        }
+
+
+class DeleteResultImageResponse(BaseModel):
+    """删除结果图片响应模型"""
+    success: bool
+    message: str
+
+
+class DashboardStatsResponse(BaseModel):
+    """仪表盘统计数据响应模型"""
+    today_generated: int = Field(..., description="今日生成数量")
+    pending_review: int = Field(..., description="待审核数量")
+    published: int = Field(..., description="已发布数量")
+
+# ==================== 中间件和依赖 ====================
+
+async def verify_user(user_id: int):
+    """验证用户是否存在且有效"""
+    user = db_ops.get_user_by_id(user_id)
+    if not user:
+        raise HTTPException(status_code=404, detail=f"用户ID {user_id} 不存在")
+    if not user.get("is_active", False):
+        raise HTTPException(status_code=403, detail="用户账户已被禁用")
+    return user
+
+
+# ==================== API接口 ====================
+
+@app.post("/api/v1/swap", response_model=SwapResponse, tags=["AI换脸换装"])
+async def process_swap(request: SwapRequest):
+    """
+    执行AI换脸换装(异步任务)
+    
+    - **user_id**: 用户ID
+    - **face_image_id**: 人脸图片ID
+    - **cloth_image_id**: 服装图片ID
+    - **prompt**: 提示词
+    - **face_prompt**: 人脸识别提示词(可选)
+    - **cloth_prompt**: 服装识别提示词(可选)
+    - **prompt_prompt**: 提示词优化(可选)
+    """
+    try:
+        # 验证用户
+        await verify_user(request.user_id)
+        
+        # 提交任务到队列(非阻塞)
+        task_id = ai_swap_service.submit_swap_task(
+            user_id=request.user_id,
+            face_image_id=request.face_image_id,
+            cloth_image_id=request.cloth_image_id,
+            prompt=request.prompt,
+            face_prompt=request.face_prompt,
+            cloth_prompt=request.cloth_prompt,
+            prompt_prompt=request.prompt_prompt,
+            quantity=request.quantity or 1
+        )
+        
+        logger.info(f"用户 {request.user_id} 换脸换装任务已提交,任务ID: {task_id}")
+        return SwapResponse(
+            success=True,
+            task_id=task_id,
+            process_record_id=None,
+            result_image_id=None,
+            copywriter_text=None,
+            history_prompt=None
+        )
+            
+    except HTTPException:
+        raise
+    except Exception as e:
+        logger.error(f"API处理异常: {str(e)}", exc_info=True)
+        raise HTTPException(status_code=500, detail=f"服务器内部错误: {str(e)}")
+
+
+@app.get("/api/v1/tasks/{task_id}", response_model=TaskStatusResponse, tags=["任务管理"])
+async def get_task_status(task_id: str):
+    """
+    获取任务状态
+    
+    - **task_id**: 任务ID
+    """
+    try:
+        task_info = ai_swap_service.get_task_status(task_id)
+        if not task_info:
+            raise HTTPException(status_code=404, detail="任务不存在")
+        
+        return TaskStatusResponse(
+            task_id=task_info["id"],
+            status=task_info["status"].value,
+            progress=task_info["progress"],
+            created_at=task_info["created_at"].isoformat(),
+            started_at=task_info["started_at"].isoformat() if task_info["started_at"] else None,
+            completed_at=task_info["completed_at"].isoformat() if task_info["completed_at"] else None,
+            result=task_info["result"],
+            error=task_info["error"]
+        )
+        
+    except HTTPException:
+        raise
+    except Exception as e:
+        logger.error(f"获取任务状态异常: {str(e)}", exc_info=True)
+        raise HTTPException(status_code=500, detail=f"服务器内部错误: {str(e)}")
+
+
+@app.get("/api/v1/users/{user_id}/tasks", response_model=UserTasksResponse, tags=["任务管理"])
+async def get_user_tasks(user_id: int):
+    """
+    获取用户的所有任务
+    
+    - **user_id**: 用户ID
+    """
+    try:
+        # 验证用户
+        await verify_user(user_id)
+        
+        tasks = ai_swap_service.get_user_tasks(user_id)
+        
+        return UserTasksResponse(
+            tasks=tasks,
+            total=len(tasks)
+        )
+        
+    except HTTPException:
+        raise
+    except Exception as e:
+        logger.error(f"获取用户任务异常: {str(e)}", exc_info=True)
+        raise HTTPException(status_code=500, detail=f"服务器内部错误: {str(e)}")
+
+
+@app.delete("/api/v1/tasks/{task_id}", tags=["任务管理"])
+async def cancel_task(task_id: str):
+    """
+    取消任务
+    
+    - **task_id**: 任务ID
+    """
+    try:
+        success = ai_swap_service.cancel_task(task_id)
+        if not success:
+            raise HTTPException(status_code=404, detail="任务不存在或无法取消")
+        
+        return {"success": True, "message": "任务已取消"}
+        
+    except HTTPException:
+        raise
+    except Exception as e:
+        logger.error(f"取消任务异常: {str(e)}", exc_info=True)
+        raise HTTPException(status_code=500, detail=f"服务器内部错误: {str(e)}")
+
+
+@app.get("/api/v1/tasks/queue/stats", tags=["任务管理"])
+async def get_queue_stats():
+    """
+    获取任务队列统计信息
+    """
+    try:
+        stats = ai_swap_service.task_queue.get_queue_stats()
+        return stats
+        
+    except Exception as e:
+        logger.error(f"获取队列统计异常: {str(e)}", exc_info=True)
+        raise HTTPException(status_code=500, detail=f"服务器内部错误: {str(e)}")
+
+
+@app.get("/api/v1/users/{user_id}/process-history", response_model=ProcessHistoryResponse, tags=["历史记录"])
+async def get_user_process_history(
+    user_id: int,
+    page: int = 1,
+    page_size: int = 20,
+    user: Dict[str, Any] = Depends(verify_user)
+):
+    """
+    获取用户的处理历史记录
+    
+    - **user_id**: 用户ID
+    - **page**: 页码(默认1)
+    - **page_size**: 每页大小(默认20,最大100)
+    """
+    try:
+        # 限制每页大小
+        page_size = min(page_size, 100)
+        
+        # 获取历史记录
+        history = ai_swap_service.get_user_process_history(user_id, page, page_size)
+        
+        return ProcessHistoryResponse(
+            records=history["records"],
+            total=history["total"],
+            page=history["page"],
+            page_size=history["page_size"],
+            total_pages=history["total_pages"]
+        )
+        
+    except Exception as e:
+        logger.error(f"获取用户历史记录失败: {str(e)}", exc_info=True)
+        raise HTTPException(status_code=500, detail=f"获取历史记录失败: {str(e)}")
+
+
+@app.get("/api/v1/process/{process_id}", response_model=ProcessDetailResponse, tags=["历史记录"])
+async def get_process_detail(
+    process_id: int,
+    user_id: Optional[int] = None
+):
+    """
+    获取处理记录详情
+    
+    - **process_id**: 处理记录ID
+    - **user_id**: 用户ID(可选,用于权限验证)
+    """
+    try:
+        # 如果提供了用户ID,先验证用户
+        if user_id:
+            await verify_user(user_id)
+        
+        # 获取处理详情
+        detail = ai_swap_service.get_process_detail(process_id, user_id)
+        
+        if not detail:
+            raise HTTPException(status_code=404, detail="处理记录不存在或无权限访问")
+        
+        return ProcessDetailResponse(
+            process_record=detail["process_record"],
+            face_image=detail["face_image"],
+            cloth_image=detail["cloth_image"],
+            result_image=detail["result_image"]
+        )
+        
+    except HTTPException:
+        raise
+    except Exception as e:
+        logger.error(f"获取处理详情失败: {str(e)}", exc_info=True)
+        raise HTTPException(status_code=500, detail=f"获取处理详情失败: {str(e)}")
+
+
+@app.put("/api/v1/process/{process_id}/text", response_model=ProcessDetailResponse, tags=["历史记录"])
+async def update_process_text(
+    process_id: int,
+    request: UpdateTextRequest = Body(...),
+):
+    """
+    更新处理记录的文本内容
+    
+    Args:
+        process_id: 处理记录ID
+        request: 更新文本请求数据
+        
+    Returns:
+        ProcessDetailResponse: 更新后的处理记录详情
+    """
+    try:
+        # 验证用户
+        user = await verify_user(request.user_id)
+        
+        # 获取处理详情
+        detail = ai_swap_service.get_process_detail(process_id, request.user_id)
+        
+        if not detail:
+            raise HTTPException(status_code=404, detail="处理记录不存在")
+            
+        # 验证权限
+        if detail["process_record"]["user_id"] != request.user_id:
+            raise HTTPException(status_code=403, detail="无权限修改此记录")
+        
+        # 更新文本内容
+        generated_text = f"- 标题:{request.title}\n- 文案:{request.content}\n- 话题标签:{request.label}"
+        
+        # 更新数据库记录
+        update_data = {
+            "generated_text": generated_text
+        }
+        
+        updated_record = db_ops.update_process_record(process_id, update_data)
+        
+        if not updated_record:
+            raise HTTPException(status_code=500, detail="更新数据库失败")
+        
+        # 重新获取更新后的记录
+        detail = ai_swap_service.get_process_detail(process_id, request.user_id)
+        
+        if not detail:
+            raise HTTPException(status_code=500, detail="获取更新后的记录失败")
+        
+        return ProcessDetailResponse(
+            process_record=detail["process_record"],
+            face_image=detail["face_image"],
+            cloth_image=detail["cloth_image"],
+            result_image=detail["result_image"]
+        )
+        
+    except HTTPException:
+        raise
+    except Exception as e:
+        logger.error(f"更新处理记录文本失败: {str(e)}", exc_info=True)
+        raise HTTPException(status_code=500, detail=f"更新处理记录文本失败: {str(e)}")
+
+
+@app.post("/api/v1/process/{process_id}/approve", response_model=ApproveRecordResponse, tags=["历史记录"])
+async def approve_process_record(
+    process_id: int,
+    request: ApproveRecordRequest = Body(...)
+):
+    """
+    审核处理记录
+    
+    Args:
+        process_id: 处理记录ID
+        request: 审核请求数据
+        
+    Returns:
+        ApproveRecordResponse: 审核结果
+    """
+    try:
+        # 验证用户
+        await verify_user(request.user_id)
+        
+        # 获取处理详情
+        detail = ai_swap_service.get_process_detail(process_id, request.user_id)
+        
+        if not detail:
+            raise HTTPException(status_code=404, detail="处理记录不存在")
+            
+        # 验证权限
+        if detail["process_record"]["user_id"] != request.user_id:
+            raise HTTPException(status_code=403, detail="无权限审核此记录")
+            
+        # 审核通过
+        success = ai_swap_service.approve_process_record(process_id)
+        
+        if not success:
+            raise HTTPException(status_code=500, detail="审核失败")
+        
+        # 重新获取审核后的记录
+        detail = ai_swap_service.get_process_detail(process_id, request.user_id)
+        
+        if not detail:
+            raise HTTPException(status_code=500, detail="获取审核后的记录失败")
+        
+        return ApproveRecordResponse(
+            success=True,
+            message="处理记录已审核通过",
+            process_record=detail["process_record"]
+        )
+        
+    except HTTPException:
+        raise
+    except Exception as e:
+        logger.error(f"审核处理记录失败: {str(e)}", exc_info=True)
+        raise HTTPException(status_code=500, detail=f"审核处理记录失败: {str(e)}")
+
+
+@app.delete("/api/v1/process/{process_id}/result-image", response_model=DeleteResultImageResponse, tags=["历史记录"])
+async def delete_result_image(
+    process_id: int,
+    request: DeleteResultImageRequest = Body(...)
+):
+    """
+    删除处理记录的结果图片
+    
+    Args:
+        process_id: 处理记录ID
+        request: 删除请求数据
+        
+    Returns:
+        DeleteResultImageResponse: 删除结果
+    """
+    try:
+        # 验证用户
+        await verify_user(request.user_id)
+        
+        # 获取处理详情
+        detail = ai_swap_service.get_process_detail(process_id, request.user_id)
+        
+        if not detail:
+            raise HTTPException(status_code=404, detail="处理记录不存在")
+            
+        # 验证权限
+        if detail["process_record"]["user_id"] != request.user_id:
+            raise HTTPException(status_code=403, detail="无权限删除此图片")
+            
+        # 删除图片
+        success = ai_swap_service.delete_result_image(process_id)
+        
+        if not success:
+            raise HTTPException(status_code=500, detail="删除图片失败")
+        
+        return DeleteResultImageResponse(
+            success=True,
+            message="结果图片已删除"
+        )
+        
+    except HTTPException:
+        raise
+    except Exception as e:
+        logger.error(f"删除结果图片失败: {str(e)}", exc_info=True)
+        raise HTTPException(status_code=500, detail=f"删除结果图片失败: {str(e)}")
+
+
+@app.get("/api/v1/users/{user_id}/images", tags=["图片管理"])
+async def get_user_images(
+    user_id: int,
+    image_type: Optional[str] = None,
+    page: int = 1,
+    page_size: int = 20,
+    user: Dict[str, Any] = Depends(verify_user)
+):
+    """
+    获取用户的图片列表
+    
+    - **user_id**: 用户ID
+    - **image_type**: 图片类型(face/cloth/result,可选)
+    - **page**: 页码(默认1)
+    - **page_size**: 每页大小(默认20,最大100)
+    """
+    try:
+        # 限制每页大小
+        page_size = min(page_size, 100)
+        
+        # 验证图片类型
+        if image_type and image_type not in ["face", "cloth", "result", "original"]:
+            raise HTTPException(status_code=400, detail="图片类型必须是 face、cloth、result 或 original")
+        
+        # 获取图片列表
+        images = db_ops.get_user_images(user_id, image_type, page, page_size)
+        
+        return {
+            "images": images["images"],
+            "total": images["total"],
+            "page": images["page"],
+            "page_size": images["page_size"],
+            "total_pages": images["total_pages"]
+        }
+        
+    except HTTPException:
+        raise
+    except Exception as e:
+        logger.error(f"获取用户图片列表失败: {str(e)}", exc_info=True)
+        raise HTTPException(status_code=500, detail=f"获取图片列表失败: {str(e)}")
+
+
+@app.get("/api/v1/dashboard/stats", response_model=DashboardStatsResponse, tags=["仪表盘"])
+async def get_dashboard_stats(user_id: int):
+    """
+    获取仪表盘统计数据
+    
+    Args:
+        user_id: 用户ID
+    
+    Returns:
+        DashboardStatsResponse: 包含今日生成、待审核、已发布的数量
+    """
+    try:
+        with db_ops.db_connection.get_session() as session:
+            # 获取今天的开始时间(0点)
+            today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
+
+            # 取该用户所有记录的关键字段,按组合(face+cloth)聚合
+            rows = (
+                session.query(
+                    ProcessRecord.face_image_id,
+                    ProcessRecord.cloth_image_id,
+                    ProcessRecord.status,
+                    ProcessRecord.completed_at,
+                    ProcessRecord.result_image_id,
+                )
+                .filter(ProcessRecord.user_id == user_id)
+                .all()
+            )
+
+            # 组合 -> 最新记录(以 completed_at 最大为准)
+            latest_by_group = {}
+            # 今日生成的组合集合(任意一条记录今天完成即可计一次)
+            groups_generated_today = set()
+
+            for face_id, cloth_id, status, completed_at, result_image_id in rows:
+                if completed_at and completed_at >= today_start and result_image_id is not None:
+                    groups_generated_today.add((face_id, cloth_id))
+
+                key = (face_id, cloth_id)
+                prev = latest_by_group.get(key)
+                if prev is None:
+                    latest_by_group[key] = (status, completed_at, result_image_id)
+                else:
+                    _, prev_time, _ = prev
+                    # None 视为更早
+                    if (prev_time or datetime.min) < (completed_at or datetime.min):
+                        latest_by_group[key] = (status, completed_at, result_image_id)
+
+            # 以组合为单位统计
+            today_generated = len(groups_generated_today)
+
+            pending_review = 0
+            published = 0
+            for (_face_id, _cloth_id), (status, _t, _rid) in latest_by_group.items():
+                if status == "待审核":
+                    pending_review += 1
+                elif status == "已发布":
+                    published += 1
+
+            return DashboardStatsResponse(
+                today_generated=today_generated,
+                pending_review=pending_review,
+                published=published,
+            )
+            
+    except Exception as e:
+        logger.error(f"获取仪表盘统计数据失败: {str(e)}", exc_info=True)
+        raise HTTPException(status_code=500, detail=f"获取仪表盘统计数据失败: {str(e)}")
+
+
+@app.get("/api/v1/statistics", tags=["系统统计"])
+async def get_system_statistics():
+    """
+    获取系统统计信息
+    """
+    try:
+        stats = db_ops.get_statistics()
+        return {
+            "success": True,
+            "statistics": stats
+        }
+        
+    except Exception as e:
+        logger.error(f"获取系统统计失败: {str(e)}", exc_info=True)
+        raise HTTPException(status_code=500, detail=f"获取系统统计失败: {str(e)}")
+
+
+# ==================== 健康检查 ====================
+
+@app.get("/health", tags=["系统"])
+async def health_check():
+    """健康检查接口"""
+    return {"status": "healthy", "service": "ai_swap_api"}
+
+
+@app.get("/")
+def root():
+    return {"msg": "AI换脸换装服务已启动"}
+
+
+# ==================== 静态文件直链下载(包含CORS响应头) ====================
+
+@app.get("/api/v1/output/{filename}")
+async def get_output_file(filename: str):
+    """为结果图片提供带CORS头的直链下载"""
+    file_path = os.path.join(output_dir, filename)
+    if not os.path.isfile(file_path):
+        raise HTTPException(status_code=404, detail="文件不存在")
+    headers = {
+        "Access-Control-Allow-Origin": "*",
+        "Access-Control-Expose-Headers": "Content-Disposition",
+    }
+    return FileResponse(file_path, headers=headers, filename=filename)
+
+
+@app.get("/api/v1/materials/{filename}")
+async def get_material_file(filename: str):
+    """为素材图片提供带CORS头的直链下载"""
+    file_path = os.path.join(materials_dir, filename)
+    if not os.path.isfile(file_path):
+        raise HTTPException(status_code=404, detail="文件不存在")
+    headers = {
+        "Access-Control-Allow-Origin": "*",
+        "Access-Control-Expose-Headers": "Content-Disposition",
+    }
+    return FileResponse(file_path, headers=headers, filename=filename)
+
+
+# ==================== 错误处理 ====================
+
+@app.exception_handler(HTTPException)
+async def http_exception_handler(request, exc):
+    """HTTP异常处理器"""
+    return JSONResponse(
+        status_code=exc.status_code,
+        content={
+            "success": False,
+            "error": exc.detail,
+            "error_type": "HTTPException"
+        }
+    )
+
+
+@app.exception_handler(Exception)
+async def general_exception_handler(request, exc):
+    """通用异常处理器"""
+    logger.error(f"未处理的异常: {str(exc)}", exc_info=True)
+    return JSONResponse(
+        status_code=500,
+        content={
+            "success": False,
+            "error": "服务器内部错误",
+            "error_type": "InternalServerError"
+        }
+    )
+
+
+# ==================== 启动配置 ====================
+
+if __name__ == "__main__":
+    """
+    启动API服务器
+    """
+    host = "0.0.0.0"
+    port = 8002
+    reload = True
+
+    print(f"启动AI Swap API服务器...")
+    print(f"服务器地址: http://{host}:{port}")
+    print(f"API文档: http://{host}:{port}/docs")
+    print(f"自动重载: {reload}")
+
+    uvicorn.run(
+        "backend.api.ai_swap_api:app",
+        host=host,
+        port=port,
+        reload=reload,
+        log_level="info"
+    ) 

+ 116 - 0
backend/api/ai_swap_bg_api.py

@@ -0,0 +1,116 @@
+"""
+AI换脸换装API接口
+提供RESTful API接口,支持换脸换装、历史记录查询等功能
+"""
+
+from fastapi import FastAPI, HTTPException, Depends, UploadFile, File, Form, Body, APIRouter
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import JSONResponse
+from starlette.responses import FileResponse
+from fastapi.staticfiles import StaticFiles
+import os
+from pydantic import BaseModel, Field
+from typing import Optional, List, Dict, Any, Union
+import uvicorn
+from datetime import datetime, timedelta
+from pydantic import BaseModel
+from sqlalchemy import func, and_
+
+from backend.services.ai_swap_bg_service import AISwapBgService, process_swap_bg_with_record
+from backend.modules.database.operations import DatabaseOperations
+from backend.utils.logger_config import setup_logger
+from backend.api.auth_api import router as auth_router
+from backend.api.user_material_api import router as user_material_router
+from backend.api.user_template_api import router as user_template_router
+from backend.modules.database.models import ProcessRecord
+from backend.api.auto_post_api import router as auto_post_router
+
+
+logger = setup_logger(__name__)
+
+router = APIRouter()
+
+# 创建服务实例
+ai_swap_bg_service = AISwapBgService()
+db_ops = DatabaseOperations()
+
+# 启动任务队列服务
+from backend.services.task_queue_service import get_task_queue_service
+task_queue_service = get_task_queue_service()
+task_queue_service.start()
+
+# ==================== 数据模型 ====================
+
+class SwapBgRequest(BaseModel):
+    user_id: int = Field(..., description="用户ID")
+    image_id: int = Field(..., description="图片ID")
+    prompt: str = Field(..., description="提示词", max_length=500)
+    quantity: Optional[int] = Field(1, ge=1, le=10, description="每组生成数量")
+    
+    
+class SwapBgResponse(BaseModel):
+    success: bool
+    task_id: Optional[str] = None
+    process_record_id: Optional[int] = None
+    result_image_id: Optional[int] = None
+    copywriter_text: Optional[str] = None
+    history_prompt: Optional[str] = None
+    error: Optional[str] = None
+    error_type: Optional[str] = None
+
+# ==================== 中间件和依赖 ====================
+
+async def verify_user(user_id: int):
+    """验证用户是否存在且有效"""
+    user = db_ops.get_user_by_id(user_id)
+    if not user:
+        raise HTTPException(status_code=404, detail=f"用户ID {user_id} 不存在")
+    if not user.get("is_active", False):
+        raise HTTPException(status_code=403, detail="用户账户已被禁用")
+    return user
+
+# ==================== API路由 ====================
+@router.post("/api/v1/swap-bg", response_model=SwapBgResponse, tags=["AI换背景"])
+async def process_swap_bg(request: SwapBgRequest):
+    """
+    执行AI换背景(异步任务)
+    
+    - **user_id**: 用户ID
+    - **image_id**: 图片ID
+    - **prompt**: 提示词
+    - **quantity**: 每组生成数量(可选)
+    """
+    try:
+        # 验证用户
+        await verify_user(request.user_id)
+        
+        # 提交任务到队列(非阻塞)
+        task_id = ai_swap_bg_service.submit_swap_bg_task(
+            user_id=request.user_id,
+            image_id=request.image_id,
+            prompt=request.prompt,
+            quantity=request.quantity or 1
+        )
+        
+        logger.info(f"用户 {request.user_id} 换背景任务已提交,任务ID: {task_id}")
+        return SwapBgResponse(
+            success=True,
+            task_id=task_id,
+            process_record_id=None,
+            result_image_id=None,
+            copywriter_text=None,
+            history_prompt=None
+        )
+            
+    except HTTPException:
+        raise
+    except Exception as e:
+        logger.error(f"API处理异常: {str(e)}", exc_info=True)
+        raise HTTPException(status_code=500, detail=f"服务器内部错误: {str(e)}")
+
+
+
+
+
+
+

+ 107 - 0
backend/api/ai_swap_cloth_api.py

@@ -0,0 +1,107 @@
+"""
+AI换衣服API接口
+提供RESTful API接口,支持AI换衣服、历史记录查询等功能
+"""
+
+import os
+import uvicorn
+from pydantic import BaseModel, Field
+from sqlalchemy import func, and_
+from datetime import datetime, timedelta
+from typing import Optional, List, Dict, Any, Union
+from starlette.responses import FileResponse
+
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import JSONResponse
+from fastapi.staticfiles import StaticFiles
+from fastapi import FastAPI, HTTPException, Depends, UploadFile, File, Form, Body, APIRouter
+
+from backend.services.ai_swap_cloth_service import AISwapClothService, process_swap_cloth_with_record
+from backend.modules.database.operations import DatabaseOperations
+from backend.utils.logger_config import setup_logger
+from backend.api.auth_api import router as auth_router
+from backend.api.user_material_api import router as user_material_router
+from backend.api.user_template_api import router as user_template_router
+from backend.modules.database.models import ProcessRecord
+from backend.api.auto_post_api import router as auto_post_router
+
+logger = setup_logger(__name__)
+
+router = APIRouter()
+
+# 创建服务实例
+ai_swap_cloth_service = AISwapClothService()
+db_ops = DatabaseOperations()
+
+# 启动任务队列服务
+from backend.services.task_queue_service import get_task_queue_service
+task_queue_service = get_task_queue_service()
+task_queue_service.start()
+
+# ==================== 数据模型 ====================
+
+class SwapClothRequest(BaseModel):
+    user_id: int = Field(..., description="用户ID")
+    raw_image_id: int = Field(..., description="原始图片ID")
+    cloth_image_id: int = Field(..., description="衣服图片ID")
+    quantity: Optional[int] = Field(1, ge=1, le=10, description="每组生成数量")
+
+class SwapClothResponse(BaseModel):
+    success: bool
+    task_id: Optional[str] = None
+    process_record_id: Optional[int] = None
+    result_image_id: Optional[int] = None
+    copywriter_text: Optional[str] = None
+    history_prompt: Optional[str] = None
+    error: Optional[str] = None
+    error_type: Optional[str] = None
+
+# ==================== 中间件和依赖 ====================
+
+async def verify_user(user_id: int):
+    """验证用户是否存在且有效"""
+    user = db_ops.get_user_by_id(user_id)
+    if not user:
+        raise HTTPException(status_code=404, detail=f"用户ID {user_id} 不存在")
+    if not user.get("is_active", False):
+        raise HTTPException(status_code=403, detail="用户账户已被禁用")
+    return user
+
+# ==================== API路由 ====================
+@router.post("/api/v1/swap-cloth", response_model=SwapClothResponse, tags=["AI换衣服"])
+async def process_swap_cloth(request: SwapClothRequest):
+    """
+    执行AI换衣服(异步任务)
+    
+    - **user_id**: 用户ID
+    - **raw_image_id**: 原始图片ID
+    - **cloth_image_id**: 衣服图片ID
+    - **quantity**: 每组生成数量(可选)
+    """
+    try:
+        # 验证用户
+        await verify_user(request.user_id)
+
+        # 提交任务到队列(非阻塞)
+        task_id = ai_swap_cloth_service.submit_swap_cloth_task(
+            user_id=request.user_id,
+            raw_image_id=request.raw_image_id,
+            cloth_image_id=request.cloth_image_id,
+            quantity=request.quantity or 1
+        )
+
+        logger.info(f"用户 {request.user_id} 换衣服任务已提交,任务ID: {task_id}")
+        return SwapClothResponse(
+            success=True,
+            task_id=task_id,
+            process_record_id=None,
+            result_image_id=None,
+            copywriter_text=None,
+            history_prompt=None
+        )
+
+    except HTTPException:
+        raise
+    except Exception as e:
+        logger.error(f"AI换衣服API处理异常: {str(e)}", exc_info=True)
+        raise HTTPException(status_code=500, detail=f"服务器内部错误: {str(e)}")

+ 107 - 0
backend/api/ai_swap_face_api.py

@@ -0,0 +1,107 @@
+"""
+AI换脸API接口
+提供RESTful API接口,支持换脸、历史记录查询等功能
+"""
+
+import os
+import uvicorn
+from pydantic import BaseModel, Field
+from datetime import datetime, timedelta
+from typing import Optional, List, Dict, Any, Union
+from starlette.responses import FileResponse
+from sqlalchemy import func, and_
+from fastapi.responses import JSONResponse
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.staticfiles import StaticFiles
+from fastapi import FastAPI, HTTPException, Depends, UploadFile, File, Form, Body, APIRouter
+
+from backend.services.ai_swap_face_service import AISwapFaceService, process_swap_face_with_record
+from backend.modules.database.operations import DatabaseOperations
+from backend.utils.logger_config import setup_logger
+from backend.api.auth_api import router as auth_router
+from backend.api.user_material_api import router as user_material_router
+from backend.api.user_template_api import router as user_template_router
+from backend.modules.database.models import ProcessRecord
+from backend.api.auto_post_api import router as auto_post_router
+
+logger = setup_logger(__name__)
+
+router = APIRouter()
+
+# 创建服务实例
+ai_swap_face_service = AISwapFaceService()
+db_ops = DatabaseOperations()
+
+# 启动任务队列服务
+from backend.services.task_queue_service import get_task_queue_service
+task_queue_service = get_task_queue_service()
+task_queue_service.start()
+
+# ==================== 数据模型 ====================
+
+class SwapFaceRequest(BaseModel):
+    user_id: int = Field(..., description="用户ID")
+    raw_image_id: int = Field(..., description="原始图片ID")
+    face_image_id: int = Field(..., description="人脸图片ID")
+    quantity: Optional[int] = Field(1, ge=1, le=10, description="每组生成数量")
+    
+    
+class SwapFaceResponse(BaseModel):
+    success: bool
+    task_id: Optional[str] = None
+    process_record_id: Optional[int] = None
+    result_image_id: Optional[int] = None
+    copywriter_text: Optional[str] = None
+    history_prompt: Optional[str] = None
+    error: Optional[str] = None
+    error_type: Optional[str] = None
+
+# ==================== 中间件和依赖 ====================
+
+async def verify_user(user_id: int):
+    """验证用户是否存在且有效"""
+    user = db_ops.get_user_by_id(user_id)
+    if not user:
+        raise HTTPException(status_code=404, detail=f"用户ID {user_id} 不存在")
+    if not user.get("is_active", False):
+        raise HTTPException(status_code=403, detail="用户账户已被禁用")
+    return user
+
+# ==================== API路由 ====================
+@router.post("/api/v1/swap-face", response_model=SwapFaceResponse, tags=["AI换脸"])
+async def process_swap_face(request: SwapFaceRequest):
+    """
+    执行AI换脸(异步任务)
+    
+    - **user_id**: 用户ID
+    - **raw_image_id**: 原始图片ID
+    - **face_image_id**: 人脸图片ID
+    - **quantity**: 每组生成数量
+    """
+    try:
+        # 验证用户
+        await verify_user(request.user_id)
+
+        # 提交任务到队列(非阻塞)
+        task_id = ai_swap_face_service.submit_swap_face_task(
+            user_id=request.user_id,
+            raw_image_id=request.raw_image_id,
+            face_image_id=request.face_image_id,
+            quantity=request.quantity or 1
+        )
+
+        logger.info(f"用户 {request.user_id} 换脸任务已提交,任务ID: {task_id}")
+        return SwapFaceResponse(
+            success=True,
+            task_id=task_id,
+            process_record_id=None,
+            result_image_id=None,
+            copywriter_text=None,
+            history_prompt=None
+        )
+
+    except HTTPException:
+        raise
+    except Exception as e:
+        logger.error(f"AI换脸API处理异常: {str(e)}", exc_info=True)
+        raise HTTPException(status_code=500, detail=str(e))

+ 64 - 0
backend/api/auth_api.py

@@ -0,0 +1,64 @@
+from fastapi import APIRouter, Request, Depends, HTTPException
+from fastapi.responses import JSONResponse
+from pydantic import BaseModel
+from typing import Optional
+from backend.services.auth_service import auth_service
+
+router = APIRouter()
+
+class RegisterRequest(BaseModel):
+    username: str
+    password: str
+    is_admin: Optional[bool] = False
+
+class LoginRequest(BaseModel):
+    username: str
+    password: str
+
+class UpdateUserRequest(BaseModel):
+    username: Optional[str] = None
+    password: Optional[str] = None
+    is_admin: Optional[bool] = None
+    is_active: Optional[bool] = None
+
+@router.post("/register")
+def register(req: RegisterRequest):
+    result = auth_service.register_user(req.username, req.password, req.is_admin)
+    if not result["success"]:
+        raise HTTPException(status_code=400, detail=result["error"])
+    return result
+
+@router.post("/login")
+def login(req: LoginRequest):
+    result = auth_service.login_user(req.username, req.password)
+    if not result["success"]:
+        raise HTTPException(status_code=401, detail=result["error"])
+    return result
+
+@router.post("/logout")
+def logout(token: str):
+    result = auth_service.logout_user(token)
+    if not result["success"]:
+        raise HTTPException(status_code=401, detail=result["error"])
+    return result
+
+@router.get("/user/{user_id}")
+def get_user_info(user_id: int):
+    result = auth_service.get_user_info(user_id)
+    if not result["success"]:
+        raise HTTPException(status_code=404, detail=result["error"])
+    return result
+
+@router.put("/user/{user_id}")
+def update_user_info(user_id: int, req: UpdateUserRequest):
+    result = auth_service.update_user_info(user_id, **req.dict(exclude_unset=True))
+    if not result["success"]:
+        raise HTTPException(status_code=400, detail=result["error"])
+    return result
+
+@router.delete("/user/{user_id}")
+def delete_user(user_id: int):
+    result = auth_service.delete_user(user_id)
+    if not result["success"]:
+        raise HTTPException(status_code=400, detail=result["error"])
+    return result 

+ 392 - 0
backend/api/auto_post_api.py

@@ -0,0 +1,392 @@
+import os
+import json
+from typing import List, Optional, Dict, Any
+from fastapi import APIRouter, HTTPException, UploadFile, File, Form, BackgroundTasks
+from pydantic import BaseModel, Field, validator
+from datetime import datetime
+
+import time
+import threading
+
+import sys
+current_dir = os.path.dirname(os.path.abspath(__file__))
+backend_dir = os.path.dirname(current_dir)
+if backend_dir not in sys.path:
+    sys.path.insert(0, backend_dir)
+
+from services.auto_post_service import auto_post_service
+from modules.database.operations import DatabaseOperations
+from utils.logger_config import setup_logger
+
+logger = setup_logger(__name__)
+router = APIRouter()
+
+# 创建线程锁防止并发冲突
+post_lock = threading.Lock()
+db_ops = DatabaseOperations()
+
+# ============== 任务状态托管 ==============
+# 简单的内存任务注册表(进程内有效,重启后清空)
+_task_registry_lock = threading.Lock()
+_task_registry: Dict[str, Dict[str, Any]] = {}
+
+
+class AutoPostTaskItemStatus(BaseModel):
+    index: int
+    schedule_time: Optional[str] = None
+    status: str = "pending"  # pending | running | success | failure
+    error: Optional[str] = None
+    started_at: Optional[str] = None
+    completed_at: Optional[str] = None
+
+
+class AutoPostTaskStatus(BaseModel):
+    task_id: str
+    name: str
+    platform: str
+    frequency: Optional[str] = None
+    scheduled_times: List[str] = []
+    total: int
+    success: int
+    failure: int
+    next_run: Optional[str] = None
+    last_run: Optional[str] = None
+    is_completed: bool = False
+    created_at: str
+    started_at: Optional[str] = None
+    completed_at: Optional[str] = None
+    items: List[AutoPostTaskItemStatus] = []
+
+
+class BatchPostItem(BaseModel):
+    image_paths: List[str]
+    title: str
+    description: str
+    topics: List[str]
+    schedule_time: Optional[str] = None
+    if_clean: bool = False
+    # 新增:该发布项关联的处理记录ID列表(同一组合内的拆分记录)
+    record_ids: List[int] = []
+
+
+class BatchPostRequest(BaseModel):
+    name: str = Field("定时发布")
+    platform: str = Field(..., description="发布平台,如 xiaohongshu")
+    frequency: Optional[str] = None
+    scheduled_times: List[str] = []
+    tasks: List[BatchPostItem]
+    # 新增:创建该任务的用户ID,用于任务隔离
+    user_id: Optional[int] = None
+
+
+def _save_task_status(task_id: str, status: Dict[str, Any]) -> None:
+    with _task_registry_lock:
+        _task_registry[task_id] = status
+
+
+def _get_task_status(task_id: str) -> Optional[Dict[str, Any]]:
+    with _task_registry_lock:
+        return _task_registry.get(task_id)
+
+
+def _list_tasks(user_id: Optional[int] = None) -> List[Dict[str, Any]]:
+    with _task_registry_lock:
+        tasks = list(_task_registry.values())
+        if user_id is not None:
+            tasks = [t for t in tasks if t.get("user_id") == user_id]
+        # 返回按创建时间倒序的列表
+        return sorted(tasks, key=lambda x: x.get("created_at", ""), reverse=True)
+
+class XiaohongshuPostRequest(BaseModel):
+    """小红书发布请求数据结构"""
+    image_paths: List[str]
+    title: str
+    description: str
+    topics: List[str]
+    schedule_time: Optional[str] = None
+    if_clean: bool = False
+
+class PostResponse(BaseModel):
+    """API响应数据结构"""
+    success: bool
+    message: str
+    task_id: Optional[str] = None
+    timestamp: str
+
+class CreateBatchTaskResponse(BaseModel):
+    success: bool
+    message: str
+    task_id: str
+    total_tasks: int
+    timestamp: str
+
+def _run_post_task(req: XiaohongshuPostRequest) -> None:
+    """在后台线程中执行发布任务,避免阻塞事件循环。"""
+    try:
+        with post_lock:
+            auto_post_service.post_to_xiaohongshu(
+                image_paths=req.image_paths,
+                title=req.title,
+                description=req.description,
+                topics=req.topics,
+                schedule_time=req.schedule_time,
+                if_clean=req.if_clean,
+            )
+        logger.info("Background post task finished successfully")
+    except Exception as e:
+        logger.exception(f"Background post task failed: {str(e)}")
+
+
+@router.post("/xiaohongshu/post", response_model=PostResponse, tags=["发布管理"])
+async def post_to_xiaohongshu(request: XiaohongshuPostRequest, background_tasks: BackgroundTasks):
+    """
+    发布内容到小红书
+    - **image_paths**: 图片路径列表
+    - **title**: 内容标题
+    - **description**: 详细描述
+    - **topics**: 话题标签列表
+    - **schedule_time**: 定时发布时间 (格式: YYYY-MM-DD HH:MM)
+    - **if_clean**: 是否清理浏览器缓存 (默认False)
+    """
+
+    task_id = f"task_{int(time.time())}"
+    logger.info(f"Received new post task {task_id}")
+
+    # 验证图片路径
+    for path in request.image_paths:
+        if not os.path.exists(path):
+            logger.error(f"Image not found: {path}")
+            raise HTTPException(
+                status_code=400,
+            )
+
+    # 验证时间格式
+    if request.schedule_time:
+        try:
+            datetime.strptime(request.schedule_time, "%Y-%m-%d %H:%M")
+        except ValueError:
+            logger.error(f"Invalid time format: {request.schedule_time}")
+            raise HTTPException(
+                status_code=400,
+                detail="时间格式错误,请使用 YYYY-MM-DD HH:MM 格式"
+            )
+    
+    # 启动独立线程执行耗时任务,避免阻塞事件循环
+    worker = threading.Thread(target=_run_post_task, args=(request,), daemon=True)
+    worker.start()
+
+    return {
+        "success": True,
+        "message": "发布任务已开始执行",
+        "task_id": task_id,
+        "timestamp": datetime.now().isoformat(),
+    }
+
+
+# ============== 批量任务接口:后端托管任务状态 ==============
+
+def _validate_item_paths(item: BatchPostItem) -> None:
+    for path in item.image_paths:
+        if not os.path.exists(path):
+            logger.error(f"Image not found: {path}")
+            raise HTTPException(status_code=400)
+
+
+def _run_batch_post_task(task_id: str) -> None:
+    status = _get_task_status(task_id)
+    if not status:
+        return
+    try:
+        status["started_at"] = datetime.now().isoformat()
+        _save_task_status(task_id, status)
+
+        items: List[Dict[str, Any]] = status["items"]
+        total = len(items)
+        for idx, item in enumerate(items):
+            # 更新当前项状态
+            item["status"] = "running"
+            item["started_at"] = datetime.now().isoformat()
+            _save_task_status(task_id, status)
+
+            try:
+                # 使用锁保护底层浏览器自动化(独占)
+                with post_lock:
+                    auto_post_service.post_to_xiaohongshu(
+                        image_paths=item["image_paths"],
+                        title=item["title"],
+                        description=item["description"],
+                        topics=item["topics"],
+                        schedule_time=item.get("schedule_time"),
+                        if_clean=item.get("if_clean", False),
+                    )
+
+                # 成功
+                item["status"] = "success"
+                item["completed_at"] = datetime.now().isoformat()
+                status["success"] += 1
+                status["last_run"] = item.get("schedule_time")
+                status["next_run"] = items[idx + 1].get("schedule_time") if idx < total - 1 else None
+
+                # 成功后批量更新该组合内的所有处理记录为“已发布”
+                try:
+                    record_ids: List[int] = item.get("record_ids", []) or []
+                    for pid in record_ids:
+                        try:
+                            db_ops.update_process_record(int(pid), {"status": "已发布"})
+                        except Exception as ue:
+                            logger.warning(f"更新处理记录状态失败 process_id={pid}: {ue}")
+                except Exception as ge:
+                    logger.warning(f"批量更新组合关联记录状态失败: {ge}")
+            except Exception as e:
+                logger.exception(f"Batch post item failed: {e}")
+                item["status"] = "failure"
+                item["error"] = str(e)
+                item["completed_at"] = datetime.now().isoformat()
+                status["failure"] += 1
+                status["last_run"] = item.get("schedule_time")
+                status["next_run"] = items[idx + 1].get("schedule_time") if idx < total - 1 else None
+
+            # 每个子任务结束都保存一次
+            _save_task_status(task_id, status)
+
+        status["is_completed"] = True
+        status["completed_at"] = datetime.now().isoformat()
+        _save_task_status(task_id, status)
+    except Exception as e:
+        logger.exception(f"Batch task failed: {e}")
+        status["is_completed"] = True
+        status["completed_at"] = datetime.now().isoformat()
+        status["error"] = str(e)
+        _save_task_status(task_id, status)
+
+
+@router.post("/tasks/batch", response_model=CreateBatchTaskResponse, tags=["发布管理"])
+async def create_batch_post_task(request: BatchPostRequest):
+    # 验证各项图片与时间格式
+    for item in request.tasks:
+        _validate_item_paths(item)
+        if item.schedule_time:
+            try:
+                datetime.strptime(item.schedule_time, "%Y-%m-%d %H:%M")
+            except ValueError:
+                raise HTTPException(status_code=400, detail="时间格式错误,请使用 YYYY-MM-DD HH:MM 格式")
+
+    task_id = f"task_{int(time.time() * 1000)}"
+    created_at = datetime.now().isoformat()
+
+    # 初始化任务状态
+    items_status: List[Dict[str, Any]] = []
+    for idx, item in enumerate(request.tasks):
+        items_status.append({
+            "index": idx,
+            "schedule_time": item.schedule_time,
+            "status": "pending",
+            "error": None,
+            "started_at": None,
+            "completed_at": None,
+            # 原始数据,供执行用
+            "image_paths": item.image_paths,
+            "title": item.title,
+            "description": item.description,
+            "topics": item.topics,
+            "if_clean": item.if_clean,
+            "record_ids": item.record_ids or [],
+        })
+
+    status: Dict[str, Any] = {
+        "task_id": task_id,
+        "name": request.name,
+        "platform": request.platform,
+        "frequency": request.frequency,
+        "scheduled_times": request.scheduled_times,
+        "total": len(items_status),
+        "success": 0,
+        "failure": 0,
+        "next_run": items_status[0].get("schedule_time") if items_status else None,
+        "last_run": None,
+        "is_completed": False,
+        "created_at": created_at,
+        "started_at": None,
+        "completed_at": None,
+        "items": items_status,
+        "user_id": request.user_id,
+    }
+
+    _save_task_status(task_id, status)
+
+    # 启动后台线程执行任务
+    worker = threading.Thread(target=_run_batch_post_task, args=(task_id,), daemon=True)
+    worker.start()
+
+    return CreateBatchTaskResponse(
+        success=True,
+        message="批量发布任务已创建",
+        task_id=task_id,
+        total_tasks=len(items_status),
+        timestamp=created_at,
+    )
+
+
+@router.get("/tasks/{task_id}/status", response_model=AutoPostTaskStatus, tags=["发布管理"])
+async def get_auto_post_task_status(task_id: str):
+    status = _get_task_status(task_id)
+    if not status:
+        raise HTTPException(status_code=404, detail="任务不存在")
+    # 过滤掉执行用原始字段,仅返回状态相关
+    sanitized_items = []
+    for item in status["items"]:
+        sanitized_items.append({
+            "index": item["index"],
+            "schedule_time": item.get("schedule_time"),
+            "status": item.get("status"),
+            "error": item.get("error"),
+            "started_at": item.get("started_at"),
+            "completed_at": item.get("completed_at"),
+        })
+
+    return AutoPostTaskStatus(
+        task_id=status["task_id"],
+        name=status.get("name", "定时发布"),
+        platform=status.get("platform", "xiaohongshu"),
+        frequency=status.get("frequency"),
+        scheduled_times=status.get("scheduled_times", []),
+        total=status.get("total", 0),
+        success=status.get("success", 0),
+        failure=status.get("failure", 0),
+        next_run=status.get("next_run"),
+        last_run=status.get("last_run"),
+        is_completed=status.get("is_completed", False),
+        created_at=status.get("created_at"),
+        started_at=status.get("started_at"),
+        completed_at=status.get("completed_at"),
+        items=sanitized_items,
+    )
+
+
+@router.get("/tasks", tags=["发布管理"])
+async def list_auto_post_tasks(user_id: Optional[int] = None):
+    tasks = _list_tasks(user_id)
+    result = []
+    for status in tasks:
+        result.append({
+            "task_id": status["task_id"],
+            "name": status.get("name"),
+            "platform": status.get("platform"),
+            "frequency": status.get("frequency"),
+            "scheduled_times": status.get("scheduled_times", []),
+            "total": status.get("total", 0),
+            "success": status.get("success", 0),
+            "failure": status.get("failure", 0),
+            "next_run": status.get("next_run"),
+            "last_run": status.get("last_run"),
+            "is_completed": status.get("is_completed", False),
+            "created_at": status.get("created_at"),
+            "started_at": status.get("started_at"),
+            "completed_at": status.get("completed_at"),
+        })
+    return {"tasks": result, "total": len(result)}
+
+
+if __name__ == "__main__":
+    abs_path = os.path.abspath(__file__).replace("\\", "/").split("backend")[0]
+    print(abs_path)

+ 79 - 0
backend/api/user_material_api.py

@@ -0,0 +1,79 @@
+from fastapi import APIRouter, UploadFile, File, Form, HTTPException, Depends
+from fastapi.responses import JSONResponse
+from typing import Optional
+from backend.services.user_material_service import user_material_service
+
+router = APIRouter()
+
+@router.post("/api/v1/users/{user_id}/materials", tags=["用户素材管理"])
+def upload_material(
+    user_id: int,
+    file: UploadFile = File(...),
+    image_type: str = Form(...),
+    original_filename: str = Form(...)
+):
+    """
+    用户上传素材
+    """
+    file_bytes = file.file.read()
+    result = user_material_service.upload_material(
+        user_id=user_id,
+        file_bytes=file_bytes,
+        image_type=image_type,
+        original_filename=original_filename
+    )
+    if not result["success"]:
+        raise HTTPException(status_code=400, detail=result["error"])
+    return result
+
+@router.get("/api/v1/users/{user_id}/materials", tags=["用户素材管理"])
+def list_materials(
+    user_id: int,
+    material_type: Optional[str] = None,
+    page: int = 1,
+    page_size: int = 20
+):
+    """
+    获取用户素材列表
+    """
+    result = user_material_service.list_materials(
+        user_id=user_id,
+        material_type=material_type,
+        page=page,
+        page_size=page_size
+    )
+    if not result["success"]:
+        raise HTTPException(status_code=400, detail=result["error"])
+    return result
+
+from pydantic import BaseModel
+
+class UpdateMaterialRequest(BaseModel):
+    original_filename: str
+
+@router.put("/api/v1/users/{user_id}/materials/{material_id}", tags=["用户素材管理"])
+def update_material(
+    user_id: int,
+    material_id: int,
+    data: UpdateMaterialRequest
+):
+    """
+    更新用户素材信息
+    """
+    result = user_material_service.update_material(user_id, material_id, data.dict())
+    if not result["success"]:
+        raise HTTPException(status_code=400, detail=result["error"])
+    return result
+
+@router.delete("/api/v1/users/{user_id}/materials/{material_id}", tags=["用户素材管理"])
+def delete_material(
+    user_id: int,
+    material_id: int
+):
+    """
+    删除用户素材
+    """
+    result = user_material_service.delete_material(user_id, material_id)
+    if not result["success"]:
+        raise HTTPException(status_code=400, detail=result["error"])
+    return result 

+ 98 - 0
backend/api/user_template_api.py

@@ -0,0 +1,98 @@
+from fastapi import APIRouter, Depends, HTTPException, Form, File
+from fastapi.responses import JSONResponse
+from typing import Optional
+from backend.services.user_template_service import user_template_service
+
+router = APIRouter()
+
+@router.post("/api/v1/users/{user_id}/templates", tags=["用户文本模板管理"])
+def create_text_template(
+    user_id: int,
+    text_type: str = Form(...),
+    text_name: str = Form(...),
+    text_label: str = Form(...),
+    text_content: str = Form(...)
+):
+    """
+    创建文本模板
+    """
+    result = user_template_service.create_text_template(
+        user_id=user_id,
+        text_type=text_type,
+        text_name=text_name,
+        text_label=text_label,
+        text_content=text_content
+    )
+    if not result["success"]:   
+        raise HTTPException(status_code=400, detail=result["error"])
+    return result
+
+@router.get("/api/v1/users/{user_id}/templates", tags=["用户文本模板管理"])
+def list_text_templates(
+    user_id: int,
+    text_type: Optional[str] = None,
+    page: int = 1,
+    page_size: int = 20
+):
+    """
+    获取用户文本模板列表
+    """
+    result = user_template_service.list_text_templates(
+        user_id=user_id,
+        text_type=text_type,
+        page=page,
+        page_size=page_size
+    )
+    if not result["success"]:
+        raise HTTPException(status_code=400, detail=result["error"])
+    return result
+
+@router.delete("/api/v1/users/{user_id}/templates/{text_id}", tags=["用户文本模板管理"])
+def delete_text_template(
+    user_id: int,
+    text_id: int
+):
+    """
+    删除文本模板
+    """
+    result = user_template_service.delete_text_template(
+        user_id=user_id,
+        text_id=text_id
+    )
+    if not result["success"]:
+        raise HTTPException(status_code=400, detail=result["error"])
+    return result
+
+@router.put("/api/v1/users/{user_id}/templates/{text_id}", tags=["用户文本模板管理"])
+def update_text_template(
+    user_id: int,
+    text_id: int,
+    text_type: Optional[str] = Form(None),
+    text_name: Optional[str] = Form(None),
+    text_label: Optional[str] = Form(None),
+    text_content: Optional[str] = Form(None)
+):
+    """
+    更新文本模板(任意字段可选,提供则更新)
+    """
+    data = {}
+    if text_type is not None:
+        data["text_type"] = text_type
+    if text_name is not None:
+        data["text_name"] = text_name
+    if text_label is not None:
+        data["text_label"] = text_label
+    if text_content is not None:
+        data["text_content"] = text_content
+
+    if not data:
+        return {"success": True, "message": "无更新字段"}
+
+    result = user_template_service.update_text_template(
+        user_id=user_id,
+        text_id=text_id,
+        data=data
+    )
+    if not result["success"]:
+        raise HTTPException(status_code=400, detail=result["error"])
+    return result

+ 3 - 0
backend/config/ai_gen_video.json

@@ -0,0 +1,3 @@
+{
+    "output_dir": "backend/output"
+}

+ 5 - 0
backend/config/ai_swap_bg_config.json

@@ -0,0 +1,5 @@
+{
+    "server_address": "60.165.238.181:8188",
+    "workflowfile": "backend/workflow/ai_swap_bg_1014.json",
+    "output_dir": "backend/output"
+}

+ 5 - 0
backend/config/ai_swap_cloth_config.json

@@ -0,0 +1,5 @@
+{
+    "server_address": "60.165.238.181:8188",
+    "workflowfile": "backend/workflow/ai_swap_cloth_1014.json",
+    "output_dir": "backend/output"
+}

+ 5 - 0
backend/config/ai_swap_face_config.json

@@ -0,0 +1,5 @@
+{
+    "server_address": "60.165.238.181:8188",
+    "workflowfile": "backend/workflow/ai_swap_face.json",
+    "output_dir": "backend/output"
+}

+ 65 - 0
backend/config/prompt_config.py

@@ -0,0 +1,65 @@
+FACE_PROMPT = """
+## 角色:你是一个人物形象描绘专家
+## 任务:请对图像中的人物脸部特征进行描述 ,需要有肤色、脸型(椭圆脸、方脸、瓜子脸、圆脸)、发型、发色、眼睛、鼻子、表情、神情等五官特征描述,并判断其人种(亚洲人\欧美人),输出字数在160字以内
+"""
+
+CLOTH_PROMPT = f"""
+## 角色:优秀的服装设计专家
+## 任务:详细描述图像中的服装细节,包括衣领(V领、圆领、方领、一字领、异形领,立领等),袖子,口袋,版型,设计;以及各个部位长度、整体衣服长度等,但不需要描述服装上的图案花纹等
+## 示例:无袖圆领连衣裙,吊带比较细长,裙子长度覆盖到脚踝处,裙子版型为A字形,圆领设计,领口到脖子处,有口袋设计,束腰腰带设计。
+## 要求:
+- 输出字数控制在250字以内,不要超过250字
+- 不能照搬示例中的描述,而是需要根据图像中的服装细节,进行实际描述,不要出现重复的描述
+"""
+
+PROMPT_PROMPT = f"""
+## 角色:优秀的提示词专家,专注于优化文生图提示词,提升画面美感
+## 任务:根据用户输入的提示词,想象并描述一副完整的画面,转化为详细的prompt,以生成高质量的图像(需要避免人物动作、姿态、表情、神情等描述与历史提示词记录相像,需要有创新性)
+## 历史提示词记录:
+---
+{{history}}
+---
+## 优秀提示词案例:A beautiful young woman with long wavy brown hair, soft hazel eyes, and a gentle smile, wearing a vintage floral maxi dress and a straw hat. She sits peacefully in a lush, sun-dappled meadow with wildflowers and distant mountains. Shot with a shallow depth of field for cinematic intimacy, warm golden hour lighting creating soft glows and long shadows, evoking a dreamy, serene atmosphere. 
+## Prompt生成步骤:
+1. 理解主题:
+- 接收输入:接收用户提供的文本
+- 分析内容:分析主题内容,确定画面主题、场景、风格等要素
+2. 画面描述,参考优秀提示词案例,使用自然语言进行描述:
+- 分析历史提示词记录,生成与历史提示词记录不同的人物姿态、动作描述,且必须是人物正面或者侧面,不要生成人物背面、背影的描述
+- 补充完整画面描述,包括人物、场景、氛围、光影、景别、镜头等,使画面描述更加完整,不能丢失人物脸部特征描述以及服装细节描述;景别尽量使用近景、中景,展现人物的全身或者半身,不要使用远景、全景等景别
+3. 输出提示词:
+- 将画面描述转化为提示词,使用英文长句的形式进行描述,不要使用中文
+- 提示词字数在150-400字之间,英文输出优化后的结果
+
+## 注意:
+- 参考优秀提示词案例的语法、句式、段落结构进行输出。
+- 输出的提示词需为纯文本形式,不要使用Markdown格式/Markdown语法/Markdown符号,不要有任何额外的内容。
+- 注意人物的姿态/动作描述一定不能与历史提示词记录相像,需要有创新性,这是基本底线,否则会生成重复的画面。
+"""
+
+FLOWER_TEXT_PROMPT = """
+## 从输入的文案中提取并二创有关服装营销宣传的短语(可以是解释服装的款式、材质、版型、上身效果、穿搭感觉等),并进行润色,使其更加符合小红书营销宣传的文案风格。
+## 只需要输出润色后的短语,不需要其他内容。
+## 输出案例:
+输出案例1:法式浪漫连衣裙
+输出案例2:天丝冰感连衣裙
+输出案例3:收腰显动人身材
+## 要求:
+- 只需输出1条短语,不要输出任何额外的内容,短语字数不能超过10个字,且不能出现重复的短语!
+- 不能照搬输出案例中的短语,而是需要根据输入的文案,进行创作
+"""
+
+COPYWRITER_PROMPT = """
+## 角色:小红书博主,文案能力优秀
+## 任务:基于图像内容,生成小红书高赞文案,文案风格为小红书风格,需要有标题,具体文案内容和标签(例如:#法式影调 #暖色调 #法式连衣裙)。
+## 要求:
+- 字数控制在400字以内,多使用合适的emoji表情
+- 除了输出标题、具体文案内容和标签,不要输出任何额外的内容
+## 输出格式:
+- 标题:<标题文本,字数在15个字以内>
+- 文案:<文案文本,需要在适当的位置插入换行符'\n',且整个文案末尾必须插入两个换行符'\n\n'作为结束>
+- 标签:<标签文本,标签数量控制在5个以内,标签文本中不能有emoji表情>
+"""
+
+if __name__ == "__main__":
+    print(PROMPT_PROMPT.format(history="暂无历史记录"))

+ 5 - 0
backend/config/workflow_config.json

@@ -0,0 +1,5 @@
+{
+    "server_address": "60.165.238.181:8188",
+    "workflowfile": "backend/workflow/ai_swap_all_1014.json",
+    "output_dir": "backend/output"
+}

BIN
backend/db/ai_swap.db


+ 242 - 0
backend/examples/ai_swap_example.py

@@ -0,0 +1,242 @@
+"""
+AI换脸换装服务使用示例
+展示如何使用AISwapService进行换脸换装操作
+"""
+
+import os
+import sys
+import time
+from datetime import datetime
+
+# 添加项目根目录到Python路径
+sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
+
+from backend.services.ai_swap_service import AISwapService, process_swap_with_record
+from backend.modules.database.operations import DatabaseOperations
+from backend.utils.logger_config import setup_logger
+
+logger = setup_logger(__name__)
+
+
+def create_test_user():
+    """创建测试用户"""
+    db_ops = DatabaseOperations()
+    
+    # 检查用户是否已存在
+    existing_user = db_ops.get_user_by_username("test_user")
+    if existing_user:
+        logger.info(f"测试用户已存在: {existing_user['id']}")
+        return existing_user["id"]
+    
+    # 创建新用户
+    user = db_ops.create_user(
+        username="test_user",
+        password_hash="test_password_hash",
+        is_admin=False
+    )
+    
+    logger.info(f"创建测试用户成功: {user['id']}")
+    return user["id"]
+
+
+def create_test_images(user_id: int):
+    """创建测试图片记录"""
+    db_ops = DatabaseOperations()
+    
+    # 检查是否已有测试图片
+    existing_images = db_ops.get_user_images(user_id, page_size=10)
+    if existing_images["total"] >= 2:
+        logger.info("测试图片已存在")
+        face_images = [img for img in existing_images["images"] if img["image_type"] == "face"]
+        cloth_images = [img for img in existing_images["images"] if img["image_type"] == "cloth"]
+        
+        if face_images and cloth_images:
+            return face_images[0]["id"], cloth_images[0]["id"]
+    
+    # 创建测试图片记录(这里只是创建记录,实际文件需要手动上传)
+    # 在实际项目中,你需要先上传图片文件,然后创建记录
+    
+    # 模拟图片路径
+    test_face_path = "backend/data/face.png"
+    test_cloth_path = "backend/data/cloth.png"
+    
+    if not os.path.exists(test_face_path) or not os.path.exists(test_cloth_path):
+        logger.warning("测试图片文件不存在,请先准备测试图片")
+        return None, None
+    
+    # 创建人脸图片记录
+    face_record = db_ops.create_image_record(
+        user_id=user_id,
+        image_type="face",
+        original_filename="test_face.png",
+        stored_path=test_face_path,
+        file_size=os.path.getsize(test_face_path)
+    )
+    
+    # 创建服装图片记录
+    cloth_record = db_ops.create_image_record(
+        user_id=user_id,
+        image_type="cloth",
+        original_filename="test_cloth.png",
+        stored_path=test_cloth_path,
+        file_size=os.path.getsize(test_cloth_path)
+    )
+    
+    logger.info(f"创建测试图片记录成功: 人脸={face_record['id']}, 服装={cloth_record['id']}")
+    return face_record["id"], cloth_record["id"]
+
+
+def test_ai_swap_service():
+    """测试AI换脸换装服务"""
+    logger.info("开始测试AI换脸换装服务")
+    
+    try:
+        # 1. 创建测试用户
+        user_id = create_test_user()
+        
+        # 2. 创建测试图片记录
+        face_image_id, cloth_image_id = create_test_images(user_id)
+        
+        if not face_image_id or not cloth_image_id:
+            logger.error("无法获取测试图片,测试终止")
+            return
+        
+        # 3. 创建服务实例
+        ai_swap_service = AISwapService()
+        
+        # 4. 执行换脸换装
+        logger.info("开始执行换脸换装...")
+        start_time = time.time()
+        
+        result = ai_swap_service.process_swap_with_record(
+            user_id=user_id,
+            face_image_id=face_image_id,
+            cloth_image_id=cloth_image_id,
+            prompt="美女站在海边,阳光明媚,穿着时尚的连衣裙"
+        )
+        
+        end_time = time.time()
+        processing_time = end_time - start_time
+        
+        # 5. 输出结果
+        if result["success"]:
+            logger.info("✅ 换脸换装成功!")
+            logger.info(f"处理时间: {processing_time:.2f}秒")
+            logger.info(f"处理记录ID: {result['process_record_id']}")
+            logger.info(f"结果图片ID: {result['result_image_id']}")
+            logger.info(f"生成的文案: {result['copywriter_text']}")
+            logger.info(f"历史提示词: {result['history_prompt']}")
+            
+            # 6. 查询历史记录
+            logger.info("\n查询处理历史...")
+            history = ai_swap_service.get_user_process_history(user_id, page=1, page_size=5)
+            logger.info(f"用户共有 {history['total']} 条处理记录")
+            
+            # 7. 查询处理详情
+            if history["records"]:
+                latest_record = history["records"][0]
+                detail = ai_swap_service.get_process_detail(latest_record["id"])
+                if detail:
+                    logger.info(f"最新处理详情: 记录ID={detail['process_record']['id']}")
+                    logger.info(f"人脸图片: {detail['face_image']['original_filename']}")
+                    logger.info(f"服装图片: {detail['cloth_image']['original_filename']}")
+                    logger.info(f"结果图片: {detail['result_image']['original_filename']}")
+            
+        else:
+            logger.error("❌ 换脸换装失败!")
+            logger.error(f"错误信息: {result['error']}")
+            logger.error(f"错误类型: {result['error_type']}")
+    
+    except Exception as e:
+        logger.error(f"测试过程中发生异常: {str(e)}", exc_info=True)
+
+
+def test_convenience_function():
+    """测试便捷函数"""
+    logger.info("开始测试便捷函数")
+    
+    try:
+        # 使用便捷函数
+        result = process_swap_with_record(
+            user_id=1,
+            face_image_id=1,
+            cloth_image_id=2,
+            prompt="美女在花园里拍照"
+        )
+        
+        if result["success"]:
+            logger.info("✅ 便捷函数测试成功!")
+            logger.info(f"结果: {result}")
+        else:
+            logger.error("❌ 便捷函数测试失败!")
+            logger.error(f"错误: {result['error']}")
+    
+    except Exception as e:
+        logger.error(f"便捷函数测试异常: {str(e)}", exc_info=True)
+
+
+def test_error_handling():
+    """测试错误处理"""
+    logger.info("开始测试错误处理")
+    
+    ai_swap_service = AISwapService()
+    
+    # 测试无效用户ID
+    try:
+        result = ai_swap_service.process_swap_with_record(
+            user_id=99999,  # 不存在的用户ID
+            face_image_id=1,
+            cloth_image_id=2,
+            prompt="测试"
+        )
+        logger.info(f"无效用户ID测试结果: {result}")
+    except Exception as e:
+        logger.info(f"无效用户ID测试异常: {str(e)}")
+    
+    # 测试无效图片ID
+    try:
+        result = ai_swap_service.process_swap_with_record(
+            user_id=1,
+            face_image_id=99999,  # 不存在的图片ID
+            cloth_image_id=2,
+            prompt="测试"
+        )
+        logger.info(f"无效图片ID测试结果: {result}")
+    except Exception as e:
+        logger.info(f"无效图片ID测试异常: {str(e)}")
+    
+    # 测试空提示词
+    try:
+        result = ai_swap_service.process_swap_with_record(
+            user_id=1,
+            face_image_id=1,
+            cloth_image_id=2,
+            prompt=""  # 空提示词
+        )
+        logger.info(f"空提示词测试结果: {result}")
+    except Exception as e:
+        logger.info(f"空提示词测试异常: {str(e)}")
+
+
+def main():
+    """主函数"""
+    logger.info("=" * 50)
+    logger.info("AI换脸换装服务测试开始")
+    logger.info("=" * 50)
+    
+    # 1. 测试错误处理
+    test_error_handling()
+    
+    # 2. 测试便捷函数
+    test_convenience_function()
+    
+    # 3. 测试完整服务
+    test_ai_swap_service()
+    
+    logger.info("=" * 50)
+    logger.info("AI换脸换装服务测试完成")
+    logger.info("=" * 50)
+
+
+if __name__ == "__main__":
+    main()

+ 36 - 0
backend/examples/auth_example.py

@@ -0,0 +1,36 @@
+"""
+用户认证服务使用示例
+"""
+from backend.services.auth_service import AuthService
+
+def main():
+    auth = AuthService()
+    print("=== 注册用户 ===")
+    res = auth.register_user("test_user2", "test_password")
+    print(res)
+
+    print("=== 登录用户 ===")
+    res = auth.login_user("test_user2", "test_password")
+    print(res)
+    token = res.get("token")
+    user = res.get("user")
+    user_id = user["id"] if user else None
+
+    print("=== 获取用户信息 ===")
+    res = auth.get_user_info(user_id)
+    print(res)
+
+    print("=== 更新用户信息 ===")
+    res = auth.update_user_info(user_id, is_active=False)
+    print(res)
+
+    print("=== 退出登录 ===")
+    res = auth.logout_user(token)
+    print(res)
+
+    print("=== 删除用户 ===")
+    res = auth.delete_user(user_id)
+    print(res)
+
+if __name__ == "__main__":
+    main() 

+ 35 - 0
backend/examples/user_material_example.py

@@ -0,0 +1,35 @@
+"""
+用户素材管理服务使用示例
+"""
+from backend.services.user_material_service import user_material_service
+
+# 假设有一个用户ID
+user_id = 1
+
+# 示例1:上传素材
+with open('backend/data/materials/1_0ae46a92003f4081bb53589d3a040569.jpg', 'rb') as f:
+    file_bytes = f.read()
+    upload_result = user_material_service.upload_material(
+        user_id=user_id,
+        file_bytes=file_bytes,
+        image_type='original',
+        original_filename='1_0ae46a92003f4081bb53589d3a040569.jpg',
+        stored_path='1_0ae46a92003f4081bb53589d3a040569.jpg',
+        image_hash='1_0ae46a92003f4081bb53589d3a040569.jpg'
+    )
+    print('上传结果:', upload_result)
+
+# 示例2:获取素材列表
+list_result = user_material_service.list_materials(
+    user_id=user_id,
+    material_type='cloth',
+    page=1,
+    page_size=10
+)
+print('素材列表:', list_result)
+
+# 示例3:删除素材
+if list_result.get('images'):
+    material_id = list_result['images'][0]['id']
+    delete_result = user_material_service.delete_material(user_id, material_id)
+    print('删除结果:', delete_result) 

+ 0 - 0
backend/modules/__init__.py


+ 0 - 0
backend/modules/auto_post/__init__.py


+ 192 - 0
backend/modules/auto_post/base_page.py

@@ -0,0 +1,192 @@
+import time
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.support import expected_conditions as EC
+from selenium.webdriver.common.keys import Keys
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.support import expected_conditions as EC
+from selenium.webdriver.common.keys import Keys
+
+class BasePage:
+    def __init__(self, driver):
+        self.driver = driver
+        self.wait = WebDriverWait(driver, 20)  # 增加默认等待时间
+        self.short_wait = WebDriverWait(driver, 5)  # 添加短等待
+        self.long_wait = WebDriverWait(driver, 30)  # 添加长等待
+    
+    def find_element(self, by, locator, timeout=None, check_visibility=True):
+        """等待并查找单个元素"""
+        wait = WebDriverWait(self.driver, timeout) if timeout else self.wait
+        try:
+            if check_visibility:
+                return wait.until(EC.visibility_of_element_located((by, locator)))
+            else:
+                return wait.until(EC.presence_of_element_located((by, locator)))
+        except:
+            # 如果找不到元素,打印页面状态
+            print("页面状态:", self.get_page_state())
+            raise
+    
+    def find_element_directly(self, by, locator):
+        """直接查找元素,不等待"""
+        return self.driver.find_element(by, locator)
+    
+    def find_clickable_element(self, by, locator, timeout=None):
+        """等待并查找可点击元素"""
+        wait = WebDriverWait(self.driver, timeout) if timeout else self.wait
+        try:
+            return wait.until(EC.element_to_be_clickable((by, locator)))
+        except:
+            # 如果找不到元素,打印页面状态
+            print("页面状态:", self.get_page_state())
+            raise
+            
+    def get_page_state(self):
+        """获取页面状态信息"""
+        try:
+            return {
+                'url': self.driver.current_url,
+                'ready_state': self.driver.execute_script('return document.readyState'),
+                'is_iframe': len(self.driver.find_elements(by='tag name', value='iframe')) > 0,
+                'page_source_length': len(self.driver.page_source)
+            }
+        except Exception as e:
+            return f"获取页面状态失败: {str(e)}"
+    
+    def safe_click(self, element):
+        """安全点击元素,如果普通点击失败则使用JS点击"""
+        try:
+            self.driver.execute_script("arguments[0].scrollIntoView(true);", element)
+            time.sleep(1)
+            element.click()
+        except Exception as e:
+            print(f"常规点击失败,尝试JS点击: {str(e)}")
+            self.driver.execute_script("arguments[0].click();", element)
+    
+    def safe_send_keys(self, element, text):
+        """
+        安全输入文本,支持emoji
+        使用分段输入的方式,确保emoji字符能够正确输入
+        """
+        try:
+            element.clear()
+            # 先点击确保焦点
+            element.click()
+            time.sleep(0.5)
+            
+            # 使用clipboard.js来处理复制粘贴
+            self.driver.execute_script("""
+                if (!window.clipboardJS) {
+                    window.clipboardJS = true;
+                    var script = document.createElement('script');
+                    script.src = 'https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.8/clipboard.min.js';
+                    document.head.appendChild(script);
+                }
+            """)
+            time.sleep(1)  # 等待脚本加载
+            
+            # 创建临时输入区域并复制文本
+            self.driver.execute_script("""
+                // 创建临时textarea
+                var textarea = document.createElement('textarea');
+                textarea.value = arguments[0];
+                textarea.style.position = 'fixed';
+                textarea.style.opacity = '0';
+                document.body.appendChild(textarea);
+                // 选择文本
+                textarea.select();
+                // 复制
+                document.execCommand('copy');
+                // 清理
+                document.body.removeChild(textarea);
+            """, text)
+            
+            # 模拟粘贴操作
+            element.send_keys(Keys.CONTROL + 'v')  # Windows
+            time.sleep(0.5)
+            
+        except Exception as e:
+            print(f"复杂输入方式失败,尝试直接输入: {str(e)}")
+            try:
+                # 如果复杂方式失败,回退到直接输入
+                element.clear()
+                element.send_keys(text)
+            except Exception as e2:
+                print(f"所有输入方式都失败: {str(e2)}")
+                raise
+    def __init__(self, driver):
+        self.driver = driver
+        self.wait = WebDriverWait(driver, 10)
+    
+    def find_element(self, by, locator):
+        """等待并查找单个元素"""
+        return self.wait.until(EC.presence_of_element_located((by, locator)))
+    
+    def find_element_directly(self, by, locator):
+        """等待并查找多个元素"""
+        return self.driver.find_element(by, locator)
+    
+    def find_clickable_element(self, by, locator):
+        """等待并查找可点击元素"""
+        return self.wait.until(EC.element_to_be_clickable((by, locator)))
+    
+    def safe_click(self, element):
+        """安全点击元素,如果普通点击失败则使用JS点击"""
+        try:
+            self.driver.execute_script("arguments[0].scrollIntoView(true);", element)
+            time.sleep(1)
+            element.click()
+        except Exception as e:
+            print(f"常规点击失败,尝试JS点击: {str(e)}")
+            self.driver.execute_script("arguments[0].click();", element)
+    
+    def safe_send_keys(self, element, text):
+        """
+        安全输入文本,支持emoji
+        使用分段输入的方式,确保emoji字符能够正确输入
+        """
+        try:
+            element.clear()
+            # 先点击确保焦点
+            element.click()
+            time.sleep(0.5)
+            
+            # 使用clipboard.js来处理复制粘贴
+            self.driver.execute_script("""
+                if (!window.clipboardJS) {
+                    window.clipboardJS = true;
+                    var script = document.createElement('script');
+                    script.src = 'https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.8/clipboard.min.js';
+                    document.head.appendChild(script);
+                }
+            """)
+            time.sleep(1)  # 等待脚本加载
+            
+            # 创建临时输入区域并复制文本
+            self.driver.execute_script("""
+                // 创建临时textarea
+                var textarea = document.createElement('textarea');
+                textarea.value = arguments[0];
+                textarea.style.position = 'fixed';
+                textarea.style.opacity = '0';
+                document.body.appendChild(textarea);
+                // 选择文本
+                textarea.select();
+                // 复制
+                document.execCommand('copy');
+                // 清理
+                document.body.removeChild(textarea);
+            """, text)
+            
+            # 模拟粘贴操作
+            element.send_keys(Keys.CONTROL + 'v')  # Windows
+            time.sleep(0.5)
+            
+        except Exception as e:
+            print(f"复杂输入方式失败,尝试直接输入: {str(e)}")
+            try:
+                # 如果复杂方式失败,回退到直接输入
+                element.clear()
+                element.send_keys(text)
+            except Exception as e2:
+                print(f"所有输入方式都失败: {str(e2)}")
+                raise

+ 149 - 0
backend/modules/auto_post/post.py

@@ -0,0 +1,149 @@
+import os
+import time
+import shutil
+import logging
+import psutil
+import subprocess
+from selenium import webdriver
+from selenium.webdriver.chrome.service import Service
+from .post_page import XiaohongshuPostPage
+from .post_config import Config
+
+
+def kill_chrome_processes():
+    """结束所有Chrome相关进程"""
+    try:
+        if os.name == 'nt':  # Windows
+            subprocess.run(['taskkill', '/F', '/IM', 'chrome.exe'], capture_output=True)
+            subprocess.run(['taskkill', '/F', '/IM', 'chromedriver.exe'], capture_output=True)
+        else:  # Linux/Mac
+            os.system("pkill -f chrome")
+            os.system("pkill -f chromedriver")
+        logging.info("已清理Chrome相关进程")
+    except Exception as e:
+        logging.warning(f"清理Chrome进程失败: {str(e)}")
+
+def clean_user_data(if_clean=True):
+    """清理用户数据目录"""
+    user_data_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'user_data'))
+    
+    # 检查并清理锁文件
+    lock_files = ['SingletonLock', 'SingletonCookie', 'SingletonSocket', 'Singleton*']
+    for lock_file in lock_files:
+        try:
+            lock_path = os.path.join(user_data_dir, 'Default', lock_file)
+            if os.path.exists(lock_path):
+                os.remove(lock_path)
+                logging.info(f"已删除锁文件: {lock_path}")
+        except Exception as e:
+            logging.warning(f"删除锁文件失败: {str(e)}")
+    
+    if os.path.exists(user_data_dir) and if_clean:
+        logging.info(f"清理并创建新用户数据目录: {user_data_dir}")
+        try:
+            # 先尝试删除可能导致问题的文件
+            problem_files = [
+                os.path.join(user_data_dir, 'Default', 'Preferences'),
+                os.path.join(user_data_dir, 'Default', 'Network Persistent State'),
+                os.path.join(user_data_dir, 'Default', 'Network Action Predictor'),
+                os.path.join(user_data_dir, 'Default', 'History'),
+                os.path.join(user_data_dir, 'Default', 'History-journal')
+            ]
+            for file in problem_files:
+                if os.path.exists(file):
+                    os.remove(file)
+                    logging.info(f"已删除文件: {file}")
+            
+            # 如果需要完全清理
+            if if_clean:
+                shutil.rmtree(user_data_dir)
+                os.makedirs(user_data_dir)
+                logging.info(f"已完全清理用户数据目录: {user_data_dir}")
+        except Exception as e:
+            logging.warning(f"清理用户数据目录失败: {str(e)}")
+    else:
+        logging.info(f"沿用旧用户数据目录")
+        # 确保目录存在
+        os.makedirs(user_data_dir, exist_ok=True)
+    
+    return user_data_dir
+
+def xiaohonshu_upload(if_clean=False):
+    """上传图文到小红书"""
+    driver = None
+    max_attempts = 3
+    
+    for attempt in range(max_attempts):
+        try:
+            # 清理Chrome进程和用户数据
+            kill_chrome_processes()
+            user_data_dir = clean_user_data(if_clean=False)
+            time.sleep(2)  # 等待进程完全结束
+            
+            logging.info(f"第{attempt + 1}次尝试启动浏览器,用户数据目录: {user_data_dir}")
+            
+            # 初始化WebDriver
+            options = webdriver.ChromeOptions()
+            options.add_argument(f'--user-data-dir={user_data_dir}')
+            options.add_argument('--profile-directory=Default')
+            options.add_experimental_option('excludeSwitches', ['enable-automation'])
+            options.add_experimental_option('useAutomationExtension', False)
+            options.add_experimental_option("detach", True)
+
+            
+            # 添加必要的配置
+            # options.add_argument('--headless')
+            options.add_argument('--disable-gpu')
+            options.add_argument('--no-sandbox')
+            options.add_argument('--disable-dev-shm-usage')
+            options.add_argument('--window-size=1920,1080')
+            options.add_argument('--start-maximized')
+            options.add_argument('--disable-blink-features=AutomationControlled')
+            options.add_argument('--disable-extensions')
+            options.add_argument('--disable-popup-blocking')
+            options.add_argument('--disable-notifications')
+            options.add_argument('--remote-debugging-port=0')  # 使用随机调试端口
+            
+            service = Service(Config.CHROME_DRIVER_PATH)
+            driver = webdriver.Chrome(service=service, options=options)
+            
+            # 如果成功创建driver,跳出重试循环
+            break
+            
+        except Exception as e:
+            logging.error(f"第{attempt + 1}次启动浏览器失败: {str(e)}")
+            if driver:
+                try:
+                    driver.quit()
+                except:
+                    pass
+            if attempt == max_attempts - 1:
+                raise
+            time.sleep(5)  # 等待一段时间后重试
+    
+    try:
+        post_page = XiaohongshuPostPage(driver)
+        post_page.open()
+        post_page.click_upload_button()
+        post_page.upload_images(Config._POST_CONFIG['image_paths'])
+        post_page.input_title(Config._POST_CONFIG['title'])
+        post_page.input_description(Config._POST_CONFIG['description'], Config._POST_CONFIG['topic'])
+        post_page.schedule_post(Config._POST_CONFIG['upload_time'])
+        
+    except Exception as e:
+        print(f"发生错误: {str(e)}")
+        driver.save_screenshot("error.png")
+        raise
+    finally:
+        time.sleep(10)
+        driver.quit()
+
+if __name__ == "__main__":
+    Config.set_post_config(
+        image_paths=[r"D:\桌面\研究生\组会\rednote\data\flower.jpg", r"D:\桌面\研究生\组会\rednote\data\test_img.png"],
+        title="💙被问爆的蓝色仙女裙,美到犯规!",
+        description="👗宝子们,挖到一条超绝的蓝色长裙!温柔的浅蓝色,仿佛把天空穿在了身上~V领设计巧妙修饰颈部线条,增添了一丝小性感。泡泡袖又带着点复古甜美感,谁穿谁是在逃公主!腰部的设计很贴心,能很好地勾勒出腰线,显得腰细细的~裙摆大大的,走路都带风,氛围感直接拉满!无论是日常出街还是度假穿都超合适,真的会被美到失语,闭眼入不亏~",
+        topic=["每日穿搭", "今日分享", "蓝色长裙", "仙女裙", "复古甜美", "小性感", "氛围感", "闭眼入不亏"],
+        upload_time="2025-07-09 10:26"  
+    )
+    xiaohonshu_upload(if_clean=False)

+ 42 - 0
backend/modules/auto_post/post_config.py

@@ -0,0 +1,42 @@
+# config.py
+class Config:
+
+    # 当前绝对路径
+    import os
+    BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+    
+
+    IMAGE_DIR = r"D:\桌面\研究生\组会\rednote\data"
+    CHROME_DRIVER_PATH = r"D:/下载/chromedriver-win64/chromedriver-win64/chromedriver.exe"
+    USER_DATA_DIR = r"user-data-dir=" + os.path.join(BASE_DIR, "user_data")
+    PAGE_LOAD_TIMEOUT = 40
+    ELEMENT_WAIT_TIMEOUT = 40
+    
+    # 发布配置默认值
+    _POST_CONFIG = {
+        'image_paths': [],
+        'title': "你能告诉我这是什么风格吗?",
+        'description': "每天输出一套穿搭\n",
+        'topic': ["每日穿搭", "今日分享"],
+        'upload_time': "2025-07-07 10:26"
+    }
+
+    @classmethod
+    def set_post_config(cls, **kwargs):
+        """
+        设置发布配置
+        :param kwargs: 可以包含 title, description, topic, upload_time
+        """
+        for key, value in kwargs.items():
+            if key in cls._POST_CONFIG:
+                cls._POST_CONFIG[key] = value
+            else:
+                raise ValueError(f"Invalid config key: {key}")
+
+    @classmethod
+    def get_post_config(cls):
+        """
+        获取当前的发布配置
+        :return: 当前的发布配置字典
+        """
+        return cls._POST_CONFIG.copy()  # 返回副本以防止直接修改

+ 360 - 0
backend/modules/auto_post/post_page.py

@@ -0,0 +1,360 @@
+import time
+from selenium.webdriver.common.by import By
+from selenium.common.exceptions import TimeoutException
+from selenium.webdriver.support.ui import WebDriverWait
+
+from modules.auto_post.base_page import BasePage
+
+class XiaohongshuPostPage(BasePage):
+    # 元素定位器
+    LOCATORS = {
+        'upload_button': (By.XPATH, '//*[text()="上传图文"]'),
+        'file_input': (By.XPATH, '//input[@type="file"]'),
+        'image_edit_text': (By.XPATH, '//*[contains(text(),"图片编辑")]'),
+        'title_input': (By.CLASS_NAME, 'd-text'),
+        'description_input': (By.CSS_SELECTOR, 'div.ql-editor.ql-blank'),
+        'first_topic': (By.ID, 'quill-mention-item-0'),
+        'schedule_button': (By.XPATH, '//*[text()="定时发布"]'),
+        'datetime_input': (By.XPATH, '//input[@placeholder="选择日期和时间"]'),
+        'confirm_schedule_button': (By.XPATH, '//*[text()="定时发布"][@class="d-text --color-static --color-current --size-text-paragraph d-text-nowrap d-text-ellipsis d-text-nowrap"]')
+    }
+    
+    def __init__(self, driver):
+        super().__init__(driver)
+        self.url = "https://creator.xiaohongshu.com/creator/post"
+    
+    def open(self):
+        """打开发布页面"""
+        self.driver.get(self.url)
+        # 在无头模式下,不需要maximize_window
+        # 等待页面加载完成
+        self.wait.until(lambda d: d.execute_script('return document.readyState') == 'complete')
+        print("页面加载完成")
+        # 添加额外检查
+        try:
+            self.wait.until(lambda d: len(d.find_elements(By.TAG_NAME, "iframe")) > 0 or 
+                          len(d.find_elements(*self.LOCATORS['upload_button'])) > 0)
+            print("关键元素已加载")
+        except:
+            print("未找到关键元素,但继续执行")
+        time.sleep(10)  # 保留较短的等待时间
+    
+    def handle_iframe(self):
+        """处理iframe切换"""
+        iframes = self.driver.find_elements(By.TAG_NAME, "iframe")
+        if iframes:
+            for iframe in iframes:
+                try:
+                    self.driver.switch_to.frame(iframe)
+                    if self.driver.find_elements(*self.LOCATORS['upload_button']):
+                        return True
+                    self.driver.switch_to.parent_frame()
+                except:
+                    self.driver.switch_to.default_content()
+        return False
+    
+    def click_upload_button(self):
+        """点击上传按钮"""
+        max_attempts = 3
+        for attempt in range(max_attempts):
+            try:
+                print(f"尝试点击上传按钮,第{attempt + 1}次")
+                
+                # 1. 确保回到主文档
+                self.driver.switch_to.default_content()
+                
+                # 2. 等待页面加载完成
+                self.wait.until(
+                    lambda d: d.execute_script('return document.readyState') == 'complete'
+                )
+                
+                # 3. 检查并切换iframe
+                if not self.handle_iframe():
+                    print("未找到包含上传按钮的iframe,尝试在主文档中查找")
+                
+                # 4. 尝试多种定位方式
+                locators = [
+                    (By.XPATH, '//*[text()="上传图文"]'),
+                    (By.CSS_SELECTOR, '[class*="upload"]'),  # 模糊匹配class
+                    (By.XPATH, '//button[contains(., "上传图文")]'),
+                    (By.XPATH, '//*[contains(@class, "upload")]//span[text()="上传图文"]')
+                ]
+                
+                for by, locator in locators:
+                    try:
+                        print(f"尝试定位器: {by}={locator}")
+                        # 先检查元素是否存在
+                        elements = self.driver.find_elements(by, locator)
+                        if elements:
+                            print(f"找到{len(elements)}个匹配元素")
+                            for element in elements:
+                                if element.is_displayed():
+                                    print("找到可见元素,准备点击")
+                                    # 保存截图用于调试
+                                    self.driver.save_screenshot(f"before_click_attempt_{attempt}.png")
+                                    # 确保元素可见
+                                    self.driver.execute_script("arguments[0].scrollIntoView(true);", element)
+                                    time.sleep(2)
+                                    # 尝试点击
+                                    self.safe_click(element)
+                                    return True
+                    except Exception as e:
+                        print(f"当前定位器失败: {str(e)}")
+                        continue
+                
+                print("当前尝试未成功,等待后重试")
+                time.sleep(5)  # 在重试之前等待
+                
+            except Exception as e:
+                print(f"第{attempt + 1}次尝试失败: {str(e)}")
+                self.driver.save_screenshot(f"error_attempt_{attempt}.png")
+                if attempt == max_attempts - 1:
+                    raise
+                time.sleep(5)
+        
+        raise TimeoutException("无法找到或点击上传按钮")
+    
+    def upload_images(self, image_paths):
+        """上传图片"""
+        try:
+            # 等待页面稳定
+            time.sleep(2)
+            
+            for path in image_paths:
+                max_attempts = 3
+                for attempt in range(max_attempts):
+                    try:
+                        # 直接使用driver查找元素
+                        file_input = self.driver.find_element(By.XPATH, '//input[@type="file"]')
+                        file_input.send_keys(path)
+                        print(f"已发送图片: {path}")
+                        break
+                    except Exception as e:
+                        print(f"第{attempt + 1}次尝试上传失败: {str(e)}")
+                        if attempt == max_attempts - 1:
+                            raise
+                        time.sleep(2)
+
+                # 等待当前图片上传开始
+                time.sleep(2)
+        
+            # 等待上传完成
+            while True:
+                time.sleep(3)
+                try:
+                    self.find_element(*self.LOCATORS['image_edit_text'])
+                    break
+                except:
+                    print("图片还在上传中...")
+
+        except Exception as e:
+            print(f"图片上传过程发生错误: {str(e)}")
+            self.driver.save_screenshot("upload_error.png")
+            raise
+    
+    def input_title(self, title):
+        """输入标题"""
+        title_input = self.find_element(*self.LOCATORS['title_input'])
+        self.safe_click(title_input)
+        time.sleep(3)
+        print(f"输入标题: {title}")
+        self.safe_send_keys(title_input, title)
+    
+    def input_description(self, description, topics):
+        """输入描述和话题"""
+        max_attempts = 3
+        for attempt in range(max_attempts):
+            try:
+                # 等待页面完全加载
+                self.wait.until(lambda d: d.execute_script('return document.readyState') == 'complete')
+                time.sleep(2)  # 额外等待
+                
+                # 尝试多种定位方式
+                locators = [
+                    (By.CSS_SELECTOR, 'div.ql-editor'),
+                    (By.CSS_SELECTOR, '[contenteditable="true"]'),
+                    (By.XPATH, '//div[contains(@class, "ql-editor")]'),
+                    (By.CSS_SELECTOR, 'div.ql-editor.ql-blank'),
+                    (By.CSS_SELECTOR, '.publish-editor .ql-editor')
+                ]
+                
+                desc_input = None
+                for by, locator in locators:
+                    try:
+                        elements = self.driver.find_elements(by, locator)
+                        for element in elements:
+                            if element.is_displayed():
+                                desc_input = element
+                                break
+                        if desc_input:
+                            break
+                    except:
+                        continue
+                
+                if not desc_input:
+                    print("未找到描述输入框,页面状态:", self.get_page_state())
+                    if attempt == max_attempts - 1:
+                        raise Exception("无法找到描述输入框")
+                    time.sleep(5)
+                    continue
+                
+                # 确保元素可见和可交互
+                self.driver.execute_script("arguments[0].scrollIntoView(true);", desc_input)
+                time.sleep(1)
+                
+                # 清除可能存在的内容
+                self.driver.execute_script("arguments[0].innerHTML = '';", desc_input)
+                time.sleep(1)
+                
+                # 点击并输入描述
+                self.safe_click(desc_input)
+                time.sleep(1)
+                print(f"输入描述: {description}")
+                self.safe_send_keys(desc_input, description)
+                time.sleep(2)
+
+                # 输入话题
+                for topic in topics:
+                    topic_attempts = 3
+                    for topic_attempt in range(topic_attempts):
+                        try:
+                            # 确保焦点在编辑器上
+                            self.driver.execute_script("arguments[0].focus();", desc_input)
+                            time.sleep(1)
+                            
+                            # 输入话题
+                            print(f"尝试输入话题: #{topic}")
+                            desc_input.send_keys(f" #{topic}")  # 添加空格防止与前面的文本连在一起
+                            time.sleep(3)  # 等待话题建议出现
+                            
+                            # 尝试多种方式定位话题建议
+                            topic_locators = [
+                                (By.XPATH, f'//div[contains(@class, "mention-item")][contains(text(), "{topic}")]'),
+                                (By.CSS_SELECTOR, '.mention-item'),
+                                (By.ID, 'quill-mention-item-0'),
+                                (By.XPATH, f'//*[contains(text(), "{topic}")]')
+                            ]
+                            
+                            topic_element = None
+                            for t_by, t_locator in topic_locators:
+                                try:
+                                    elements = self.driver.find_elements(t_by, t_locator)
+                                    for element in elements:
+                                        if element.is_displayed() and topic.lower() in element.text.lower():
+                                            topic_element = element
+                                            break
+                                    if topic_element:
+                                        break
+                                except:
+                                    continue
+                            
+                            if topic_element:
+                                # 使用JavaScript点击话题
+                                self.driver.execute_script("arguments[0].click();", topic_element)
+                                print(f"成功选择话题: {topic}")
+                                time.sleep(2)
+                                break
+                            else:
+                                raise Exception(f"未找到话题建议: {topic}")
+                                
+                        except Exception as e:
+                            print(f"第{topic_attempt + 1}次尝试输入话题'{topic}'失败: {str(e)}")
+                            if topic_attempt == topic_attempts - 1:
+                                print(f"跳过话题: {topic}")
+                            else:
+                                time.sleep(3)
+                                try:
+                                    # 尝试清除当前输入
+                                    desc_input.send_keys(Keys.BACKSPACE * 20)
+                                except:
+                                    pass
+                                time.sleep(2)
+                
+                return True
+                
+            except Exception as e:
+                print(f"第{attempt + 1}次尝试输入描述失败: {str(e)}")
+                if attempt == max_attempts - 1:
+                    raise
+                time.sleep(5)
+    
+    def schedule_post(self, datetime_str):
+        """设置定时发布"""
+        schedule_btn = self.find_clickable_element(*self.LOCATORS['schedule_button'])
+        self.safe_click(schedule_btn)
+        time.sleep(7)
+        
+        datetime_input = self.find_clickable_element(*self.LOCATORS['datetime_input'])
+        self.safe_send_keys(datetime_input, datetime_str)
+        time.sleep(5)
+        
+        confirm_btn = self.find_clickable_element(*self.LOCATORS['confirm_schedule_button'])
+        self.safe_click(confirm_btn)
+
+    def get_page_state(self):
+        """获取页面状态信息"""
+        try:
+            return {
+                'url': self.driver.current_url,
+                'ready_state': self.driver.execute_script('return document.readyState'),
+                'is_iframe': len(self.driver.find_elements(By.TAG_NAME, "iframe")) > 0,
+                'page_source_length': len(self.driver.page_source)
+            }
+        except Exception as e:
+            return f"获取页面状态失败: {str(e)}"
+    
+    def save_debug_info(self, prefix="debug"):
+        """保存调试信息"""
+        timestamp = time.strftime("%Y%m%d_%H%M%S")
+        try:
+            # 在无头模式下保存更多调试信息
+            debug_info = {
+                'url': self.driver.current_url,
+                'page_source_length': len(self.driver.page_source),
+                'ready_state': self.driver.execute_script('return document.readyState'),
+                'is_iframe': len(self.driver.find_elements(By.TAG_NAME, "iframe")) > 0,
+                'viewport_size': self.driver.execute_script('return [window.innerWidth, window.innerHeight];'),
+                'page_errors': self.driver.execute_script('return window.errors || [];'),
+                'network_status': self.driver.execute_script('return window.navigator.onLine;')
+            }
+            
+            # 保存截图
+            self.driver.save_screenshot(f"./logs/{prefix}_{timestamp}.png")
+            # 保存页面源码
+            with open(f"./logs/{prefix}_{timestamp}.html", "w", encoding="utf-8") as f:
+                f.write(self.driver.page_source)
+            # 保存调试信息
+            with open(f"./logs/{prefix}_{timestamp}_debug.txt", "w", encoding="utf-8") as f:
+                f.write(str(debug_info))
+        except Exception as e:
+            print(f"保存调试信息失败: {str(e)}")
+
+def xiaohonshu_upload():
+    driver = None
+    try:
+        # ... 初始化代码 ...
+        
+        post_page = XiaohongshuPostPage(driver)
+        post_page.open()
+        
+        # 添加页面状态检查
+        print("页面状态:", post_page.get_page_state())
+        
+        try:
+            post_page.click_upload_button()
+        except Exception as e:
+            print("点击上传按钮失败,保存调试信息")
+            post_page.save_debug_info("upload_button_error")
+            raise
+            
+        # ... 其他操作 ...
+        
+    except Exception as e:
+        print(f"发生错误: {str(e)}")
+        if driver:
+            driver.save_screenshot("final_error.png")
+            print("最终页面状态:", post_page.get_page_state() if 'post_page' in locals() else "页面未初始化")
+        raise
+    finally:
+        if driver:
+            driver.quit()

BIN
backend/modules/auto_post/temp/flower.jpg


BIN
backend/modules/auto_post/temp/test_img.png


+ 1 - 0
backend/modules/auto_post/user_data/AmountExtractionHeuristicRegexes/3/_metadata/verified_contents.json

@@ -0,0 +1 @@
+[{"description":"treehash per file","signed_content":{"payload":"eyJjb250ZW50X2hhc2hlcyI6W3siYmxvY2tfc2l6ZSI6NDA5NiwiZGlnZXN0Ijoic2hhMjU2IiwiZmlsZXMiOlt7InBhdGgiOiJoZXVyaXN0aWNfcmVnZXhlcy5iaW5hcnlwYiIsInJvb3RfaGFzaCI6IktFLTExaS1abVpJMGp0WDFCR1oxU3FOTkdDcXBOdkxxb0FBV2ZBeXBVcW8ifSx7InBhdGgiOiJtYW5pZmVzdC5qc29uIiwicm9vdF9oYXNoIjoid3RKemxxbFBSMUY4d1JzTGgwZHZYZ3RBRWVrNnRBU1BfV1VSSTYtaWlpVSJ9XSwiZm9ybWF0IjoidHJlZWhhc2giLCJoYXNoX2Jsb2NrX3NpemUiOjQwOTZ9XSwiaXRlbV9pZCI6Imhhamlnb3BiYmpoZ2hiZmltZ2tmbXBlbmZrY2xtb2hrIiwiaXRlbV92ZXJzaW9uIjoiMyIsInByb3RvY29sX3ZlcnNpb24iOjF9","signatures":[{"header":{"kid":"publisher"},"protected":"eyJhbGciOiJSUzI1NiJ9","signature":"lAzKHwyX4iMeWuaEeO0ymoOD4Zou9Par9qF78GYPFOzvxiw8vTc3wmYWQ4lBcwdU74-ctIQOdCKBJ-icY8I7aLo0dkKwDMMpShpWXDackfUFRfSI5L_fePBucK96_egP8lk0s2WTWIR4SqxOC1Cvp_CbxQpZ5l_BS-0NGTFqHgurQRf4Aa02aD5L51liGlJUoZrdj1eRCz_u26HIXLpiYcQ_mfbNNxRoUuBCbQqC589ecVPbQ825e736KMV31NwoEwL_sCCpHiJK5YwW0s9evPX4oRLmrWdPsILypIrkWl_SvDhRs_ZyLnPPGnF8D99BoZ0wZDm6o2ScrGByvUDdfKpk_220tQu7WPWL3cGA5AJ14pDYpGwf1kXnkdd40ZN8ysMQD8qfLEUg0EeyR-6CQklJxseCDoft46q7RXeOIDwwpZ3VD2i6gAmDw5p6HzF7r5ySLVSFZx84sjmnBLGon1NU6tS3rTcNnxPLUOTtzW2XORVFzL0bFRCly01HTMPgB4MGIVV1xUDjGv-fgAykAdoR1y7MtPj9Me_Afp0pkUwANkepQ26AenyL_XpKIUH-kBTKbV6WojpaVy7FPZdSij2FHVUUvczzoJwjnLYeN9bUzjN-fRVvjE-9vA5iXMcDTO9NWwScn3hGzeaBmc_LVtMlm_trGBcZ0OGLCta0U0M"},{"header":{"kid":"webstore"},"protected":"eyJhbGciOiJSUzI1NiJ9","signature":"DmmG355rlzyunw3Wh_yYdlGfSGq0SnDp5oFK9l0AKga88c54UwVWvnNX_9cuIHYICk_oKnLNOQkXSgu2Z5J7p5lAdaZZ2ZCmLsA2azHro8VmL9V2qli8MEWYMvE6QrpyUiUgHKumCFyjDCQUcSGWRHw_avqqroWuQgVfA6TMw_-DFC0z3nN3NSFR6cm3Kg1ir6b0OFDM0t0-c5tj7RLCXSKPharGhviwc_CSKHoTiwhC6HaXVtuH3UKvpzWpTWU9NppcyQWRwnwpqjxM5OaEHtSc8mUoXCmRe_kFnNxZb0X1N1YCLg4t-6ZtU5LAtZEz6oMdkGXF6J_fU6K_8MsHuw"}]}}]

+ 3 - 0
backend/modules/auto_post/user_data/AmountExtractionHeuristicRegexes/3/heuristic_regexes.binarypb

@@ -0,0 +1,3 @@
+
+¾
+•(?:US\$|USD|\$)\s*\d{1,3}(?:[.,]\d{3})*(?:[.,]\d{2})?(?:\s*)(?:USD|US\$|\$)?|(?:USD|US\$|\$)?\s*\d{1,3}(?:[.,]\d{3})*(?:[.,]\d{2})?\s*(?:USD|US\$|\$)¡^(?:\s*)(Due now \(USD\)|(Estimated )?(?:Order Total|Total:?)|TOTAL CHARGED TODAY \*|Final Total Price:|Flight total|grand total:?|Order Total(?: \(USD\))?:?|Price|Total(?:(?: \(USD\))?| Due| for Stay| Price| to be paid:| to pay|:)?|(Your )?(?:Payment Today|total(\s)price|Total:))(?:\s*)$

+ 5 - 0
backend/modules/auto_post/user_data/AmountExtractionHeuristicRegexes/3/manifest.json

@@ -0,0 +1,5 @@
+{
+  "manifest_version": 2,
+  "name": "Amount Extraction Heuristic Regexes",
+  "version": "3"
+}

+ 10 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/AD

@@ -0,0 +1,10 @@
+
+AD 
+canilloparròquia de canillo
+encampparròquia d'encamp&
+
+la massanaparròquia de la massana
+ordinoparròquia d'ordino<
+#parròquia de sant julià de lòriasant julià de lòria1
+andorra la vellaparròquia d'andorra la vella3
+escaldesengordanyparròquia d'escaldesengordany

+ 10 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/AE

@@ -0,0 +1,10 @@
+
+AE
+
+عجمانajmanE
+
أبو ظبي	abu dhabiأبو ظَبيإمارة أبو ظبي$
+إمارة دبيّdubaiدبي5
+إمارة الفجيرةfujairahالفجيرةE
+إمارة رأس الخيمةras al khaimahرأس الخيمةQ
+إمارة الشارقةsharjahإمارة الشارقةّالشارقةo
+إمارة أم القيوينemirate of umm al quwainإمارة ام القيوينام القيوين

+ 44 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/AF

@@ -0,0 +1,44 @@
+
+AF5
+بلخbalkhبلخ ولايتولایت بلخH
+بامیانbamyanباميان ولايتولایت بامیانI
+بادغیسbadghisبادغيس ولايتولایت بادغیسL
+بدخشان
+badakhshanبدخشان ولايتولایت بدخشانC
+
+بغلانbaghlanبغلان ولايتولایت بغلانc
+دایکندیdaykundiدايکندي ولايتدایکنډي‎ولایت دایکندی;
+فراهfarahفراه ولايتولایت فراهH
+فاریابfaryabفارياب ولايتولایت فاریاب<
+غزنيghazniغزني ولايتولایت غزنی4
+غورghorغور ولايتولایت غورC
+
+هلمندhelmandهلمند ولايتولایت هلمند;
+هراتheratهرات ولايتولایت هراتI
+جوزجانjowzjanجوزجان ولايتولایت جوزجان
+کابلkabulM
+ولایت قندهارkandaharقندهارقندھارکندهارH
+ولایت کاپیساkapisaکاپيساکاپيسا ولايت@
+كندزkunduzولایت کندوزکندوز ولايتH
+خوستkhostخوست ولايتخوست‎ولایت خوست2
+ولایت کنرkunar	کنر‎کونړ‎C
+
+لغمانlaghmanلغمان ولايتولایت لغمانG
+
+لوگَرlogarلوګرلوګر ولايتولایت لوگرd
+ننګرهار	nangarharد ننګرهار ولايتننگرهارولایت ننگرهارH
+نیمروزnimruzنيمروز ولايتولایت نیمروزP
+نورستانnuristanنورستان ولايتولایت نورستانJ
+ولایت پنجشیرpanjshirپنجشیرپنجشېر ولايتB
+ولایت پروانparwan
+پروانپروان ولايت7
+ولایت پکتیاpaktia
+پکتيا
+پکتیاI
+ولایت پکتیکاpaktikaپکتيکا ولايتپکتیکاJ
+سمنگانsamanganسمنګان ولايتولایت سمنگانD
+	سر پلsarsare polسرپل ولايتولایت سرپل<
+تخارtakharتخار ولايتولایت تخار=
+ولایت اروزگانoruzganروزګان ولايتF
+ميدان وردگwardakوردکوردګولایت وردک;
+زابلzabulزابل ولايتولایت زابل

+ 13 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/AG

@@ -0,0 +1,13 @@
+
+AG#
+saint georgesaint george parish
+
+saint johnsaint john parish
+
+saint marysaint mary parish
+
+saint paulsaint paul parish!
+saint petersaint peter parish#
+saint philipsaint philip parish	
+barbuda	
+redonda

+ 14 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/AL

@@ -0,0 +1,14 @@
+
+AL(
+beratitberat countyqarku i beratit.
+	durrësitdurrës countyqarku i durrësit.
+	elbasanitelbasan countyqarku i elbasanit*
+fierfier countyfiertqarku i fieritI
+
gjirokastrësgjirokastërgjirokastër countyqarku i gjirokastrës5
+korçëkorçë countykorçësqarku i korçës!
+qarku i kukësit
kukës county 
+qarku i lezhës
lezhë county 
+qarku i dibrës
dibër countyI
+qarku shkodërqarku i shkodrësregjioni i shkodërshkodër county+
+qarku i tiranës
tirana countytiranës1
+qarku i vlorësvlorë
vlorë countyvlorës

+ 15 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/AM

@@ -0,0 +1,15 @@
+
+AMH
+արագածոտնaragatsotn provinceարագածոտնի մարզ8
+արարատararat provinceարարատի մարզ=
+արմավիրarmavir provinceարմավիրի մարզ#
+երեւանyerevan
+երևանQ
+գեղարքունիքgegharkunik province!գեղարքունիքի մարզ8
+կոտայքkotayk provinceկոտայքի մարզ.
+լոռի
lori provinceլոռու մարզ4
+
+շիրակshirak provinceշիրակի մարզ<
+սյունիքsyunik provinceսյունիքի մարզ8
+տավուշtavush provinceտավուշի մարզG
+վայոց ձորvayots dzor provinceվայոց ձորի մարզ

+ 21 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/AO

@@ -0,0 +1,21 @@
+
+AO
+bengobengo province
+benguelabenguela province
+bié
bié province
+cabindacabinda province(
+
cuandocubangocuando cubango province
+cunenecunene province1
+cuanzanortecuanza norte provincekwanzanorte+
+	cuanzasulcuanza sul province	kwanzasul
+huambohuambo province
+huílahuila province/
+lunda nortelunda norte province
+lundanorte
+lundasullunda sul province
+luandaluanda province
+malanjemalanje province
+moxicomoxico province&
+moçâmedesnamibenamibe province
+uígeuíge province
+zairezaire province

+ 29 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/AR

@@ -0,0 +1,29 @@
+
+AR+
+provincia de saltasaltasalta province@
+buenos airesbuenos aires provinceprovincia de buenos aires_
+buenos airescabacapital federal ciudad autónoma de buenos airesciudad de buenos aires4
+provincia de san luissan luissan luis province=
+entre ríosentre ríos provinceprovincia de entre ríos4
+la riojala rioja provinceprovincia de la riojaU
+ provincia de santiago del esterosantiago del esterosantiago del estero province,
+chacochaco provinceprovincia del chaco4
+provincia de san juansan juansan juan province#
+	catamarcaprovincia de catamarca4
+la pampala pampa provinceprovincia de la pampa1
+mendozamendoza provinceprovincia de mendoza4
+misionesmisiones provinceprovincia de misiones1
+formosaformosa provinceprovincia de formosa5
+neuquénneuquén provinceprovincia del neuquén:
+provincia de río negro
+río negrorío negro province4
+provincia de santa fesanta fesanta fe province4
+provincia de tucumántucumántucumán province/
+chubutchubut provinceprovincia del chubutL
+provincia de tierra del fuegotierra del fuegotierra del fuego province:
+
+corrientescorrientes provinceprovincia de corrientes4
+córdobacórdoba provinceprovincia de córdoba+
+jujuyjujuy provinceprovincia de jujuy:
+provincia de santa cruz
+santa cruzsanta cruz province

+ 7 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/AS

@@ -0,0 +1,7 @@
+
+AS
+manu'amanu'a district
+easterneastern district
+rose island
+westernwestern district
+
swains island

+ 14 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/AT

@@ -0,0 +1,14 @@
+
+AT
+
+burgenland
+kärnten	carinthia"
+niederösterreich
lower austria-
+oberösterroberösterreich
upper austria
+
land salzburgsalzburg
+
+steiermarkstyria
+tiroltyrol
+
+vorarlberg
+wienvienna

+ 12 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/AU

@@ -0,0 +1,12 @@
+
+AU
+jervis bay territoryjbt#
+australian capital territoryact
+new south walesnsw
+northern territorynt
+
+queenslandqld
+south australiasa
+tasmaniatas
+victoriavic
+western australiawa

+ 5 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/AX

@@ -0,0 +1,5 @@
+
+AX&
+mariehamns stadmariehamn subregion!
+ålands skärgårdarchipelago 
+ålands landsbygdcountryside

+ 4 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/AZ

@@ -0,0 +1,4 @@
+
+AZM
+
+naxçıvannakhchivan autonomous republicnaxçıvan muxtar respublikası

+ 5 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BA

@@ -0,0 +1,5 @@
+
+BA
+federacija bosne i hercegovine$federation of bosnia and herzegovina9федерација босне и херцеговине?
+brčko distriktbrčko districtбрчко дистриктS
+republika srpskaрепублика српскaрепублика српска

+ 15 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BB

@@ -0,0 +1,15 @@
+
+BB%
+
christ churchchrist church parish#
+saint andrewsaint andrew parish#
+saint georgesaint george parish!
+saint jamessaint james parish
+
+saint johnsaint john parish#
+saint josephsaint joseph parish
+
+saint lucy%
+
saint michaelsaint michael parish!
+saint petersaint peter parish#
+saint philipsaint philip parish#
+saint thomassaint thomas parish

+ 10 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BD

@@ -0,0 +1,10 @@
+
+BD6
+"বরিশাল বিভাগbarisal division_
++চট্টগ্রাম বিভাগchittagong divisionচট্রগ্রাম.
+ঢাকা বিভাগdhaka division2
+খুলনা বিভাগkhulna divisionQ
+রাজশাহীrajshahi division%রাজশাহী বিভাগD
+রংপুরrangpur divisionরংপুর বিভাগ2
+সিলেট বিভাগsylhet divisionB
++ময়মনসিংহ বিভাগmymensingh division

+ 6 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BE

@@ -0,0 +1,6 @@
+
+BE\
+	bruxellesbrusselbrusselsbrussels hoofdstedelijk gewestrégion de bruxellescapitale%
+
vlaams gewestflanders
+vlaanderen&
+région wallonnewalloniawallonie

+ 18 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BF

@@ -0,0 +1,18 @@
+
+BF-
+boucle du mouhounboucle du mouhoun region
+cascadescascades region
+centre
centre region)
+
+centre est	centreestcentreest region,
+centre nord
+centrenordcentrenord region9
+centreouestcentreouest regionrégion du centreouest
+	centresudcentresud region
+est
+est region#
+hautsbassinshautsbassins region
+nordnord region8
+plateau centralplateaucentralplateaucentral region
+sahelsahel region
+sudouestsudouest region

+ 37 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BG

@@ -0,0 +1,37 @@
+
+BGS
+благоевградblagoevgrad province#област благоевград1
+бургасburgasобласт бургас,
+
+варнаvarnaобласт варна`
+велико търновоveliko tarnovo province(област велико търново,
+
+видинvidinобласт видин-
+
+врацаvratsaобласт враца6
+габровоgabrovoобласт габрово;
+добричdobrich provinceобласт добричE
+кърджалиkardzhali provinceобласт кърджалиJ
+кюстендилkyustendil provinceобласт кюстендил-
+
+ловечlovechобласт ловеч?
+монтанаmontana provinceобласт монтанаA
+област пазарджик
+pazardzhikпазарджик1
+област перникpernikперник:
+област плевенpleven provinceплевен?
+област пловдивplovdiv provinceпловдив6
+област разградrazgradразград'
+област русеruseрусе;
+област силистраsilistraсилистра
+сливенsliven province2
+област смолянsmoljanсмолянB
+област софияsofia city provinceсофияградH
+софийска областsofia provinceсофия областM
+$област стара загораstara zagoraстара загораK
+област търговищеtargovishte provinceтърговище?
+област хасковоhaskovo provinceхасково%
+област шумен
+шумен-
+област ямболjambol
+ямбол

+ 6 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BH

@@ -0,0 +1,6 @@
+
+BH2
+محافظة العاصمةcapital governorateG
+!المحافظة الجنوبيةsouthern governorateجنوبية1
+محافظة المحرقmuharraq governorateK
+الشماليةnorthern governorate!المحافظة الشمالية

+ 20 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BI

@@ -0,0 +1,20 @@
+
+BI
+province de rumongerumonge
+bubanzaprovince de bubanza.
+bujumbura ruralprovince de bujumbura ruralO
+bujumbura mairieiprovense ya bujumbura mairieprovince de bujumbura mairie
+bururiprovince de bururi
+cankuzoprovince de cankuzo 
+cibitokeprovince de cibitoke
+gitegaprovince de gitega
+kirundoprovince de kirundo
+karuziprovince de karuzi
+kayanzaprovince de kayanza
+makambaprovince de makamba 
+muramvyaprovince de muramvya
+mwaroprovince de mwaro
+muyingaprovince de muyinga
+ngoziprovince de ngozi
+province de rutanarutana
+province de ruyigiruyigi

+ 15 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BJ

@@ -0,0 +1,15 @@
+
+BJ&
+atacoraatakoraatakora department
+aliborialibori department#
+
+atlantiqueatlantique department
+borgouborgou department:
+collinescollines departmentdépartement des collines
+dongadonga department#
+couffokouffokouffo department9
+département du littorallittorallittoral department
+monomono department
+ouéméouémé department6
+département du plateauplateauplateau department
+zouzou department

+ 13 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BM

@@ -0,0 +1,13 @@
+
+BM
+pembrokepembroke parish0
+saint george'sst george's parish
+stgeorge's
+hamiltonhamilton parish
+warwickwarwick parish'
+smith's parishsmiths
smiths parish!
+southamptonsouthampton parish
+
+devonshiredevonshire parish
+sandys
sandys parish
+pagetpaget parish

+ 6 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BN

@@ -0,0 +1,6 @@
+
+BN(
+belaitbelait district
daerah belaitL
+bruneimuarabruneimuara districtdaerah brunei muaradaerah bruneimuara1
+daerah temburong	temburongtemburong district(
+
daerah tutongtutongtutong district

+ 14 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BO

@@ -0,0 +1,14 @@
+
+BO.
+benibeni departmentdepartamento del beni?
+
+cochabambacochabamba departmentdepartamento de cochabambae
+
+chuquisacachuquisaca department$departamento autónomo de chuquisacadepartamento de chuquisacaU
+ departamento autónomo de la pazdepartamento de la pazla pazla paz departmenth
+departamento autónomo de pandodepartamento de pandogobernación de pandopandopando departmentg
+departamento autónomo de orurodepartamento de orurogobernacón de oruroorurooruro departmentq
+!departamento autónomo de potosídepartamento de potosígobernación de potosípotosi departmentpotosíe
+$departamento autónomo de santa cruzdepartamento de santa cruz
+santa cruzsanta cruz departmentU
+departamento de tarija departemento autónomo de tarijatarijatarija department

+ 5 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BQ

@@ -0,0 +1,5 @@
+
+BQ
+bonaireboneiru
+saba
+sint eustatius

+ 31 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BR

@@ -0,0 +1,31 @@
+
+BR
+acreac
state of acre
+alagoasalstate of alagoas!
+amazonasamstate of amazonas
+amapáapstate of amapá"
+bahiababaíastate of bahia
+cearáce(
+distrito federaldffederal district?
+espirito santoesespírito santostate of espírito santo
+goiásgostate of goiás#
+	maranhãomastate of maranhão0
+minasmgminas geraisstate of minas gerais5
+mato grosso do sulmsstate of mato grosso do sul'
+mato grossomtstate of mato grosso
+parápastate of pará!
+paraíbapbstate of paraíba%
+
+pernambucopestate of pernambuco
+piauípistate of piauí
+paranáprstate of paranáA
+baixada fluminenserjrio de janeirostate of rio de janeiro7
+rio grande do norternstate of rio grande do norte#
+	rondôniarostate of rondônia
+roraimarrstate of roraima3
+rio grande do sulrsstate of rio grande do sul-
+santa catarinascstate of santa catarina
+sergipesestate of sergipe%
+
+são paulospstate of são paulo#
+	tocantinstostate of tocantins

+ 35 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BS

@@ -0,0 +1,35 @@
+
+BS
+new providence	
+acklins
+biminibimini and cat cay
+black point
+
berry islands
+central eleuthera
+
+cat island
+crooked island and long cay
+
central abaco
+central andros
+east grand bahama
+exuma
+city of freeportfreeport
+	grand cay
+harbour island
+	hope town
+inagua
+long island
+mangrove cay
+	mayaguana
+abacomoore's island
+north eleuthera
+north abaco
+north andros	
+rum cay
+
ragged island
+south andros
+south eleuthera
+south abaco
+san salvador
+
spanish wells
+west grand bahama

+ 24 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BT

@@ -0,0 +1,24 @@
+
+BT>
+paro
paro district'སྤ་རོ་རྫོང་ཁགH
+chhukhachukhachukha district$ཆུ་ཁ་རྫོང་ཁག	
+hahaaP
+samchisamtsesamtse district-བསམ་རྩེ་རྫོང་ཁགf
+thimphuthimphu districtthimpu*ཐིམ་ཕུ་རྫོང་ཁགཐིམ་ཕུགP
+chirangtsirangtsirang district*རྩི་རང་རྫོང་ཁགT
+dagadaganadagana district3དར་དཀར་ནང་རྫོང་ཁགJ
+punakhapunakha district-སྤུ་ན་ཁ་རྫོང་ཁགt
+wangdue phodrangwangdue phodrang districtEདབང་འདུས་ཕོ་བྲང་རྫོང་ཁགJ
+sarpangsarpang district-གསར་སྤང་རྫོང་ཁགU
+tongsatrongsatrongsa district0ཀྲོང་གསར་རྫོང་ཁགI
+bumthangbumthang district*བུམ་ཐང་རྫོང་ཁགR
+zhemgangzhemgang district3གཞམས་སྒང་རྫོང་ཁག་a
+	tashigang
+trashigangtrashigang district3བཀྲིས་སྒང་རྫོང་ཁགH
+mongarmongar district-མོང་སྒར་རྫོང་ཁགu
+
+pemagatselpemagatshelpemagatshel districtpremagalshel6པདྨ་དགའ་ཚལ་རྫོང་ཁག%
+lhuntselhuntse districtlhuntshi}
+samdrup jongkharsamdrup jongkhar districtNབསམ་གྲུབ་ལྗོངས་མཁར་རྫོང་ཁག
+gasa
gasa district'
+
tashi yangtse
trashiyangtseyangtse

+ 21 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BW

@@ -0,0 +1,21 @@
+
+BW
+francistown city
+selibe phikwe town
+lobatse town
+jwaneng town
+chobe district
+
gaborone city
+	sowa town>
+centralcentral districtkgaolo ya legarengwati district+
+ghanzighanzi districtkgaolo ya ghanziD
+	kgalagadikgalagadi district#kgalagadi le dikgaolo tse di mabapi
+kgatlengkgatleng district
+kwenengkweneng district+
+
+north east	northeastnortheast district 
+
+north westnorthwest district+
+
+south east	southeastsoutheast district8
+southernmotsana wa molapowabojangsouthern district

+ 8 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BY

@@ -0,0 +1,8 @@
+
+BYT
+!брэсцкая вобласцьbrest region!брестская областьZ
+%гомельская вобласцьgomel region#гомельская область
+)гарадзенская вобласць
hrodna region'гродзенская вобласць%гродненская область`
+'магілёўская вобласцьmogilev region%могилёвская областьN
+мінская вобласцьminsk regionминская областьX
+#віцебская вобласцьvitebsk region!витебская область

+ 8 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/BZ

@@ -0,0 +1,8 @@
+
+BZ
+belizebelize district
+cayo
cayo district
+corozalcorozal district#
+orange walkorange walk district#
+stann creekstann creek district
+toledotoledo district

+ 15 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CA

@@ -0,0 +1,15 @@
+
+CA
+albertaab
+british columbiabc
+manitobamanmb%
+
new brunswicknbnouveaubrunswick7
+labradornlnewfoundlandnewfoundland and labrador
+nova scotians
+northwest territoriesnt
+nunavutnu
+ontonontario
+peipeprince edward island
+québecqcquebec
+saskatchewansk
+yukonytyukon territory

+ 3 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CC

@@ -0,0 +1,3 @@
+
+CC(
+shire of cocos islandsshire of cocos

+ 32 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CD

@@ -0,0 +1,32 @@
+
+CD
+kwilu	
+sankuru
+kasaïkasai
+
+tanganyika-
+kasaicentralkasaï central
kasaïcentral
+tshopo
+hautkatanga	
+lualaba
+kwango
+hautuele
+hautuélé
+ituri	
+tshuapa
+	maindombe
+	sudubangi
+
+hautlomami	
+mongala
+lomami	
+basuele
+
+nordubangi'
+bascongo
kongo centralkongocentral-
+province de l'équateurequator	équateur@
+
kasaiorientalkasai orientalkasaï orientalkasaïoriental
+kinshasalipopo	
+maniema
+nordkivu	northkivu
+sudkivu	southkivu

+ 22 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CF

@@ -0,0 +1,22 @@
+
+CF
+ouham
+baminguibangoran<
+archidiocèse de banguibanguikötä gbätä tî bangî
+
+bassekotto
+
+hautekotto
+
+hautmbomou1
+mamberekadeimambérékadéimambérékadéï(
+nanagrebizinanagribizinanagrébizi
+kemokémo
+lobaye
+mbomouO
+ombella m'poko
ombellam'poko.sêse tî kömändâkötä tî ömbëläpökö<
+
nanamambéré+sêse tî kömändâkötä tî nanämambere
+ouham pendéouhampendé
+sanghambaresanghambaéré
+ouaka
+vakaga

+ 16 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CG

@@ -0,0 +1,16 @@
+
+CG	
+bouenza
+pool
+sangha
+
+plateaux
+cuvetteouest&
+pointenoirepointe noire	pwantenwa
+	lékoumoulekoumou
+kouiloukuilu
+
+likouala	
+cuvette
+niari
+brazzaville

+ 33 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CH

@@ -0,0 +1,33 @@
+
+CH
+aargauag
kanton aargau9
+appenzell innerrhodenaikanton appenzell innerrhoden;
+appenzell ausserrhodenarkanton appenzell ausserrhoden'
+bernbecanton of bernkanton bern=
+basel (kanton)blbasellandschaftkanton basellandschaft>
+
basel (stadt)bs
+basel city
+baselstadtkanton baselstadta
+freiburgfrcanton de fribourgcanton friburgofribourgfriburgfriburgokanton freiburg
+genèvegegeneva
+glarusgl
kanton glarus.
+graubündengrgrisonskanton graubünden
+canton du jurajujura$
+
kanton luzernlulucerneluzern&
+canton de neuchâtelne
+neuchâtel!
+	nidwaldennwkanton nidwalden
+kanton obwaldenowobwalden2
+kanton sankt gallensgsankt gallen	st gallen'
+kanton schaffhausenshschaffhausen!
+kanton solothurnso	solothurn
+
kanton schwyzszschwyz
+kanton thurgautgthurgau
+
canton ticinotiticino
+
+kanton uriururi
+canton de vaudvdvaudW
+
kanton wallisvscanton du valaiscanton vallesevalaisvallaisvallesewallis
+zugzg
+kanton zug%
+zürichzhkanton zürichzurich

+ 18 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CI

@@ -0,0 +1,18 @@
+
+CI:
+district autonome d'abidjanabidjan autonomous districtX
+bassassandrabassassandra districtdistrict du bassassandrarégion du bassassandra4
+comoecomoécomoé districtdistrict du comoéC
+denguele
+denguélédenguélé districtdistrict du denguéléH
+district du gôhdjiboua
+gohdjibouagôhdjibouagôhdjiboua district:
+district des lacslacs
lacs districtrégion des lacsF
+district des laguneslaguneslagunes districtrégion des lagunesX
+18 montagnesdistrict des montagnesdixhuit montagnes	montagnesmontagnes districtd
+district du sassandramarahouésassandramarahouesassandramarahouésassandramarahoué districtF
+district des savanesrégion des savanessavanessavanes districtf
+!district de la vallée du bandamavalle du bandamavallée du bandamavallée du bandama district-
+district du worobaworobaworoba districtH
+!district autonome du yamoussoukroyamoussoukroyamoussoukro district-
+district du zanzanzanzanzanzan district

+ 29 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CL

@@ -0,0 +1,29 @@
+
+CL¤
+
+11 región,aisén del general carlos ibáñez del campoaysén,aysén del general carlos ibáñez del campo4región aisén del general carlos ibáñez del campo7región de aysén del general carlos ibáñez del campo
+xi región7xi región aisén del general carlos ibáñez del campoW
+	2 regiónantofagasta
+ii regiónii región de antofagastaregión de antofagastal
+	15 regionarica y parinacotaregión de arica y parinacota
+xv región xv región de arica y parinacotat
+	9 región	araucania
+araucanía
+ix regiónix región de la araucanía
la araucaníaregión de la araucaníaM
+	3 regiónatacamaiii regióniii región de atacamaregión de atacama”
+	8 regiónbio biobiobío	bío bíoregión del biobíoregión del bío bíoregión del bíobíoviii regiónviii región del bío bíoN
+	4 regióncoquimbo
+iv regióniv región de coquimboregión de coquimbo÷
+	6 región%libertador general bernardo o'higgins'libertador general bernardo o’higgins	o'higginsregión de o’higgins3región del libertador general bernardo o’higgins
+vi región6vi región del libertador general bernardo o’higginsP
+
+10 región	los lagosregión de los lagos	x regiónx región de los lagosT
+
+14 región	los ríosregión de los ríosxiv regiónxiv región de los ríos›
+
+12 región!magallanes and chilean antarctica%magallanes y de la antártica chilena"magallanes y la antártica chilena0región de magallanes y de la antártica chilena-región de magallanes y la antártica chilenaxii región1xii región de magallanes y la antártica chilenaI
+	7 regiónmauleregión del maulevii regiónvii región del maule0
+provincia de ñubleregión de ñubleñuble�
+metropolitana de santiagoregión metropolitana!región metropolitana de santiagorm$rm región metropolitana de santiagosantiago metropolitan regionO
+	1 región	i regióni región de tarapacáregión de tarapacá	tarapacáU
+	5 regiónregión de valparaíso	v regiónv región de valparaísovalparaíso

+ 12 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CM

@@ -0,0 +1,12 @@
+
+CM:
+adamawaadamaouaadamawa regionrégion de l'adamaouaG
+centralcentre
centre regionprovince du centrerégion du centre~
+
extreme northextreme nordextrêmenord	far northfar north regionfarnorthrégion de l'extrêmenordrégion du nord=
+easteast regionestprovince de l'estrégion de l'estJ
+littoraldépartement du littorallittoral regionrégion du littoral>
+northnordnorth regionprovince du nordrégion du nord>
+	northwest	nordouestnorthwest regionrégion du nordouestC
+westouestprovince de l'ouestrégion de l'ouestwest region;
+southprovince du sudrégion du sudsouth regionsud<
+	southwestrégion du sudouestsouthwest regionsudouest

+ 33 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CN

@@ -0,0 +1,33 @@
+
+CN
+北京beijing	北京市
+天津tianjin	天津市
+冀hebei河北	河北省
+山西shanxi	山西省/
+	内蒙古inner mongolia内蒙古自治区
+辽宁liaoning	辽宁省
+吉林jilin	吉林省'
+	黑龙江heilongjiang黑龙江省
+上海shanghai	上海市!
+江苏jiangsu	江苏省苏
+浙江zhejiang	浙江省
+安徽anhui	安徽省 
+福建fujian	福建省闽!
+江西jiangxi	江西省赣"
+山东shandong	山东省鲁
+河南henan	河南省豫
+湖北hubei	湖北省鄂
+湖南hunan	湖南省湘'
+广东guangdong province	广东省3
+广西guangxi广西壮族自治区	广西省
+海南hainan	海南省
+重庆	chongqing	重庆市&
+四川sichuan	四川省川蜀&
+贵guizhou贵州	贵州省黔 
+云南yunnan	云南省滇%
+藏tibet西藏西藏自治区
+陕西shaanxi	陕西省$
+甘gansu甘肃	甘肃省陇
+青海qinghai	青海省-
+宁ningxia宁夏宁夏回族自治区1
+新xinjiang新疆新疆维吾尔自治区

+ 42 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CO

@@ -0,0 +1,42 @@
+
+CO
+
+amazonas
+	antioquia
+arauca
+
+atlántico	atlantico
+bolívarbolivar
+boyacáboyaca
+caldas
+caquetácaqueta
+
+casanare
+cauca
+cesar
+chocóchoco
+córdobacordoba
+cundinamarcaL
+bogotábogota
+bogotá dcdistrito capitaldistrito capital de bogotá
+guainíaguainia
+
+guaviare
+huila
+
+la guajira
+	magdalena
+meta
+nariñonarino%
+norte de santandernorth santander
+
+putumayo
+quindíoquindio
+	risaralda
+	santanderU
+archipiélago de san andréssan andres and providenciasan andrés y providencia
+sucre
+tolima
+valle del cauca
+vaupésvaupes	
+vichada

+ 11 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CR

@@ -0,0 +1,11 @@
+
+CR4
+alajuelaalajuela provinceprovincia de alajuela1
+cartagocartago provinceprovincia de cartago:
+
+guanacasteguanacaste provinceprovincia de guanacaste1
+herediaheredia provinceprovincia de heredia5
+limonlimónlimón provinceprovincia de limón:
+provincia de puntarenas
+puntarenaspuntarenas province7
+provincia de san josé	san josésan josé province

+ 20 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CU

@@ -0,0 +1,20 @@
+
+CU<
+pinar del río
pinar del rioprovincia de pinar del río7
+
ciudad habanahavana	la habanaprovincia la habana!
+matanzasprovincia de matanzas'
+provincia de villa claravilla clara;
+
+cienfuegosprovincia cienfuegosprovincia de cienfuegosB
+provincia de sancti spíritussancti spiritussancti spíritus?
+ciego de ávilaciego de avilaprovincia de ciego de ávila-
+	camagüeycamagueyprovincia de camagüey#
+	las tunasprovincia de las tunas*
+holguínholguinprovincia de holguín
+granmaprovincia de granma1
+provincia de santiago de cubasantiago de cuba3
+guantánamo
+guantanamoprovincia de guantánamo4
+artemisaartemisa provinceprovincia de artemisa#
+	mayabequeprovincia de mayabeque
+isla de la juventud

+ 28 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CV

@@ -0,0 +1,28 @@
+
+CV
+brava
+	boa vista
+santa catarina
+santa catarina do fogo
+
+santa cruz
+maio
+	mosteiros
+paulpaúl
+
+porto novo
+praia
+
ribeira brava
+ribeira grande
+ribeira grande de santiago
+
são domingos
+são filipe
+sao filipe
+sal
+são miguel
+são lourenço dos órgãos
+são salvador do mundo
+são vicente
+
+tarrafal3
+tarrafal de são nicolautarrafal de sao nicolau

+ 3 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CX

@@ -0,0 +1,3 @@
+
+CX
+shire of christmas island

+ 9 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CY

@@ -0,0 +1,9 @@
+
+CYZ
+!επαρχία λευκωσίαςlefkoşalefkoşa kazasınicosiaλευκωσία#
+λεμεσόςlimasollimassolT
+επαρχία λάρνακαςlarnacalarnakalarnaka kazasıλάρνακα.
+αμμόχωστος	famagustagazimağusaJ
+επαρχία πάφουbafbaf kazasıgazibafpaphos
+πάφος
+κερύνειαgirne

+ 16 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/CZ

@@ -0,0 +1,16 @@
+
+CZ
+hlavní město prahaprague.
+středočeský krajcentral bohemian region)
+jihočeský krajsouth bohemian region 
+plzeňský kraj
plzeň region(
+karlovarský krajkarlovy vary region)
+ústecký krajústí nad labem region!
+liberecký krajliberec region2
+královéhradecký krajhradec králové region$
+pardubický krajpardubice region"
+kraj vysočinavysočina region+
+jihomoravský krajsouth moravian region!
+olomoucký krajolomouc region
+zlínský krajzlín region0
+moravskoslezský krajmoraviansilesian region

+ 19 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/DE

@@ -0,0 +1,19 @@
+
+DE
+brandenburgbb
+berlinbe
+badenwürttembergbw
+bayernbybavaria%
+bremenhbfreie hansestadt bremen
+hessenhe
+hamburghh
+mecklenburgvorpommernmv"
+
niedersachsenndslower saxony0
+nordrheinwestfalennrwnorth rhinewestphalia)
+rheinlandpfalzrprhinelandpalatinate
+schleswigholsteinsh
+saarlandsl
+sachsensnsaxony!
+
sachsenanhaltsasaxonyanhalt
+
+thüringenth	thuringia

+ 11 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/DJ

@@ -0,0 +1,11 @@
+
+DJ5
+أرتاartarégion d'artaإقليم عرتاO
+إقليم على صبيح
+ali sabiehrégion d'ali sabiehعلي صبيح<
+إقليم دخيلdikhilrégion de dikhil
+دِخيل
+جيبوتيdjibouti;
+
+أوبوكobockrégion d'obockإقليم أوبوخ_
+إقليم تاجورةrégion de tadjourah	tadjourahإقليم تجرةتادجورا

+ 10 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/DK

@@ -0,0 +1,10 @@
+
+DK
+christiansø
+ertholmene7
+nordjyllandnorth denmark regionregion nordjylland9
+midtjyllandcentral denmark regionregion midtjylland;
+region syddanmarkregion of southern denmark
+syddanmark<
+hovedstadencapital region of denmarkregion hovedstaden-
+region sjællandregion zealand	sjælland

+ 16 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/DM

@@ -0,0 +1,16 @@
+
+DM#
+saint andrewsaint andrew parish!
+saint davidsaint david parish#
+saint georgesaint george parish
+
+saint johnsaint john parish#
+saint josephsaint joseph parish
+
+saint lukesaint luke parish
+
+saint marksaint mark parish%
+
saint patricksaint patrick parish
+
+saint paulsaint paul parish!
+saint petersaint peter parish

+ 36 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/DO

@@ -0,0 +1,36 @@
+
+DO
+distrito nacional
+azua
azua province,
+bahorucobaorucobaoruco provinceneiba
+barahonabarahona province
+dajabóndajabón province+
+duarteduarte provinceduarte provinciaA
+elias piñaelías piñaelías piña province
la estrelleta
+el seiboel seibo province
+	espaillatespaillat province'
+
independenciaindependencia province'
+
la altagraciala altagracia province
+	la romanala romana province
+la vegala vega province=
+maría trinidad sánchez!maría trinidad sánchez province2
+monte cristimonte cristi provincemontecristi!
+
+pedernalespedernales province
+peraviaperavia province%
+puerto platapuerto plata province6
+hermanas mirabalhermanas mirabal provincesalcedo
+samanásamaná province)
+san cristóbalsan cristóbal province
+san juansan juan provinceM
+san pedro de macorissan pedro de macoríssan pedro de macorís province/
+sánchez ramírezsánchez ramírez province
+santiagosantiago province3
+santiago rodríguezsantiago rodríguez province
+valverdevalverde province+
+monseñor nouelmonseñor nouel province#
+monte platamonte plata province!
+
+hato mayorhato mayor province/
+san josé de ocoasan josé de ocoa province'
+
santo domingosanto domingo province

+ 69 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/DZ

@@ -0,0 +1,69 @@
+
+DZC
+
+أدرارadrar provincewilaya d'adrarولاية أدرار\
+شلفchlefchlef provincewilaya de chlefولاية الشلولاية الشلفR
+الأغواطlaghouat provincewilaya de laghouatولاية الأغواطy
+أم البواقي‎oum el bouaghi province
oum elbouaghiwilaya d'oum el bouaghiولاية أم البواقي8
+ولاية باتنةbatna provincewilaya de batna\
+
+بجايةbéjaïabéjaïa provincevgayetwilaya de béjaïaولاية بجايةW
+
بسكرة‎biskra provincewilaya de biskraبِسكرةولاية بسكرةM
+بشارbécharbéchar provincewilaya de bécharولاية بشارS
+البليدةblidablida provincewilaya de blidaولاية البليدةv
+البويرةbouirabouïrabouïra provincetuvirettwilaya de bouira
+بويرةولاية البويرةp
+تمنراستwilaya de tamanghassettamanrasset provincewilaya de tamanrassetولاية تمنراست^
+تبسةtébessatébessa provincewilaya de tébessaتيبيساولاية تبسةg
+تلمسانtlemcentlemcen provincewilaya de tlemcenتلِمسِانولاية تلمسانF
+
+تيارتtiaret provincewilaya de tiaretولاية تيارت
+تيزي أوزو
+tizi ouzoutizi ouzou province	tiziouzouwilaya de tizi ouzouتيزي وزوولاية تيزي وزو
+الجزائرalgeralgiers provincewilaya d'algerالجزائر العاصمة
+دزايرولاية الجزائرJ
+الجلفةdjelfa provincewilaya de djelfaولاية الجلفة@
+جيجلjijel provincewilaya de jijelولاية جيجلJ
+سطيفsétifsétif provincewilaya de sétifولاية سطيفL
+صيداsaïdasaïda provincewilaya de saïdaولاية سعيدة[
+سكيكدةskikda provincewilaya de skikdaسكيكدة‎ولاية سكيكدة
+سيدي بلعباسsidi bel abbessidi bel abbès province
sidibelabbèswilaya de sidi bel abbèsسيدي بلعباس‎ ولاية سيدي بلعباسA
+ولاية عنابةannabaannaba provincewilaya d'annabaN
+
+قالمةguelmaguelma provincewilaya de guelmaولاية قالمة
+القسطنطينيةconstantineconstantine provincewilaya de constantineقسنطينة‎ولاية قسنطينةd
+المدية‎médéamédéa provincewilaya de médéa
+ميدياولاية المديةy
+مستغانم‎
+mostaganemmostaganem provincewilaya de mostaganemمُستَغنِمولاية مستغانمe
+المسيلة‎m'silam'sila provincewilaya de m'sila
+مسيلةولاية المسيلة`
+
+معسكرmascaramascara provincewilaya de mascara
معسكر‎ولاية معسكر\
+
+ورجلةouarglaouargla provincewilaya d'ouargla
+ورقلةولاية ورقلة^
+ولاية وهرانoran
oran provincewahren
wilaya d'oran
+وهران
وهران‎d
+
+البيضel bayadh provinceelbayadhwilaya d'el bayadh
البيض‎ولاية البيضT
+
+اليزيillizi provincewilaya d'illizi
اليزي‎ولاية إليزي
+برج بوعريريجbordj bou arréridjbordj bou arréridj provincebordjbouarreridjwilaya de bordj bou arreridjبرج بوعريريج‎"ولاية برج بوعريريجy
+بومرداس‎
+boumerdèsboumerdès provincewilaya de boumerdèsبومِردِاسولاية بومرداسf
+الطارفel taref provinceeltarefwilaya d'el tarfالطارف‎ولاية الطارفq
+
+تندوفtindouf provincewilaya de tindouf
تندوف‎ولاية تندوفولاية تندوف‎m
+تسمسيلت‎tissemsilt provincewilaya de tissemsiltتيسمسيلتولاية تيسمسيلتd
+العويضel oued provinceelouedwilaya d'el ouedالوادي‎ولاية الواديe
+ولاية خنشلة	khenchelakhenchela provincewilaya de khenchelaولاية خنشلة‎
+سوق أهراس‎souk ahras province	soukahraswilaya de souk ahrasسوق الأحراسولاية سوق أهراس^
+
+تبازةtipazatipaza provincewilaya de tipazaتيبازةولاية تيبازةK
+ميلة
mila provincewilaya de milaميلة‎ولاية ميلة
+عين الدفلى‎aïn defla province	aïndeflawilaya de aïn deflaعين الدِفلةولاية عين الدفلىt
+النعامةnaâmanaâma provincewilaya de naâmaولاية النعامةولاية النعامة‎
+عين تموشنت‎aïn témouchentaïn témouchent provincewilaya d'aïn témouchentولاية عين تموشنتL
+ولاية غردايةghardaia province	ghardaïawilaya de ghardaïaa
+رِليزانrelizane provincewilaya de relizaneغليزان‎ولاية غليزان

+ 31 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/EC

@@ -0,0 +1,31 @@
+
+EC
+azuayprovincia de azuay*
+bolívarbolivarprovincia de bolívar
+carchiprovincia de carchi!
+orellanaprovincia de orellana9
+cantón esmeraldas
+esmeraldasprovincia de esmeraldas
+cañarprovincia de cañar
+guayasprovincia del guayas%
+
+chimborazoprovincia de chimborazo!
+imbaburaprovincia de imbabura
+lojaprovincia de loja
+manabíprovincia de manabí
+napoprovincia de napo
+el oroprovincia de el oro#
+	pichinchaprovincia de pichincha-
+	los ríoslos riosprovincia de los ríos?
+morona santiagomoronasantiagoprovincia de morona santiagoo
+,provincia de santo domingo de los tsáchilassanto domingo de los tsachilassanto domingo de los tsáchilas'
+provincia de santa elenasanta elena%
+provincia de tungurahua
+tungurahua0
+provincia de sucumbíos	sucumbios
+sucumbíosL
+
+galápagosgalápagos provinceislas galápagosprovincia de galápagos!
+cotopaxiprovincia de cotopaxi
+pastazaprovincia de pastazaB
+provincia de zamora chinchipezamora chinchipezamorachinchipe

+ 17 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/EE

@@ -0,0 +1,17 @@
+
+EE
+
harju maakondharju county
+hiiu maakondhiiu county2
+idaviru maakondidaviru countyi̇dаvirumаа!
+jõgeva maakondjõgeva county
+järva maakond
järva county!
+lääne maakondlääne county)
+lääneviru maakondlääneviru county
+põlva maakond
põlva county
+pärnu maakond
pärnu county
+
rapla maakondrapla county
+
saare maakondsaare county
+
tartu maakondtartu county
+
valga maakondvalga county#
+viljandi maakondviljandi county
+
võru maakondvõru county

+ 35 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/EG

@@ -0,0 +1,35 @@
+
+EG}
+الأسكندريةalexandria governorateالإسكندريةالاسكندرية!محافظة الإسكندريةD
+
+أسوانaswan governorate
+اسوانمحافظة أسوان9
+
+أسيوطassiut governorateمحافظة أسيوطT
+البحر الأحمرred sea governorate$محافظة البحر الأحمرB
+البحيرةbeheira governorateمحافظة البحيرةW
+بنى سويفbeni suef governorateبني سويفمحافظة بني سويفC
+القاهرةcairo governorateمحافظة القاهرة‬G
+الدقهليةdakahlia governorateمحافظة الدقهلية;
+
+دمياطdamietta governorateمحافظة دمياط=
+الفيومfaiyum governorateمحافظة الفيومB
+الغربيةgharbia governorateمحافظة الغربية;
+الجيزةgiza governorateمحافظة الجيزةS
+الإسماعيليةismailia governorate#محافظة الإسماعيليةP
+جنوب سيناءsouth sinai governorate محافظة جنوب سيناءM
+القليوبيةalqalyubia governorateمحافظة القليوبيةN
+كفر الشيخkafr elsheikh governorateمحافظة كفر الشيخ/
+قناqena governorateمحافظة قناJ
+الأقصرluxor governorateالاقصرمحافظة الأقصر<
+المنياminya governorateمحافظة المنياF
+المنوفيةmenofia governorateمحافظة المنوفية@
+محافظة مطروحmarsa matrouh governorate
+مطروحD
+بورسعيدport said governorateمحافظة بورسعيد8
+
+سوهاجsohag governorateمحافظة سوهاجD
+الشرقيةalsharqia governorateمحافظة الشرقيةP
+شمال سيناءnorth sinai governorate محافظة شمال سيناء;
+السويسsuez governorateمحافظة السويس_
+الوادي الجديدthe new valley governorate&محافظة الوادي الجديد

+ 5 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/EH

@@ -0,0 +1,5 @@
+
+EH
+)الداخلة  وادي الذهب‎‎oued eddahablagouirarío de orola güera-جهة وادي الذهب  الڭويرة‎-
+guelmimes semaraكلميم السمارةa
+laâyouneboujdoursakia el hamra>جهة العيون بوجدور الساقية الحمراء

+ 11 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/ER

@@ -0,0 +1,11 @@
+
+ERN
+أنسيباansebaإقليم أنسبا
+عنسباዞባ ዓንሰባ
+1إقليم البحر الأحمر الجنوبيsouthern red sea جنوب البحر الأحمر"ديبوباوي كيه باهري'ዞባ ደቡባዊ ቀይሕ ባሕሪ7
+الجنوبيةdebub
+ديبوبዞባ ደቡብG
+جاش بركا
+gash barkaقاش بركاዞባ ጋሽ ባርካZ
+المركزيةmaekelالمنطقة المركزيةمأكلዞባ ማእከል
+ سيمناوي كيه باهريnorthern red sea شمال البحر الأحمر'ዞባ ሰሜናዊ ቀይሕ ባሕሪ

+ 22 - 0
backend/modules/auto_post/user_data/AutofillStates/2025.6.13.84507/ES

@@ -0,0 +1,22 @@
+
+ES
+
+andalucíaan	andalusia
+aragónararagon&
+asturiasasprincipado de asturias
+	cantabriacb&
+ceutaceciudad autónoma de ceuta)
+castilla y leónclcastile and león=
+castilla la manchacmcastilela manchacastillala mancha.
+canariascncanary islandsislas canarias
+	catalunyact	catalonia
+extremaduraex
+galiciagagaliza)
+
illes balearsibpmbalearic islands)
+región de murciamcregion of murcia.
+comunidad de madridmdcommunity of madrid*
+ciudad autónoma de melillamlmelilla2
+comunidad foral de navarrancnavarranavarre,
+euskadipvbasque country
euskal herria
+la riojari:
+comunidad valencianavcvalencian community	valència

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません