feature: 新增API预设-指定章节分析模型

This commit is contained in:
xiamuceer
2026-04-30 11:27:57 +08:00
parent 659d18e290
commit 46608e6e31
6 changed files with 202 additions and 19 deletions
+16 -15
View File
@@ -57,7 +57,7 @@ from app.services.memory_service import memory_service
from app.services.foreshadow_service import foreshadow_service
from app.services.chapter_regenerator import ChapterRegenerator
from app.logger import get_logger
from app.api.settings import get_user_ai_service
from app.api.settings import get_user_ai_service, get_user_ai_service_from_db_by_usage
from app.utils.sse_response import SSEResponse, create_sse_response
router = APIRouter(prefix="/chapters", tags=["章节管理"])
@@ -797,7 +797,7 @@ async def analyze_chapter_background(
user_id: str,
project_id: str,
task_id: str,
ai_service: AIService
ai_service: Optional[AIService] = None
) -> bool:
"""
后台异步分析章节(支持并发,使用锁保护数据库写入)
@@ -865,6 +865,13 @@ async def analyze_chapter_background(
task.progress = 20
await db_session.commit()
if ai_service is None:
ai_service = await get_user_ai_service_from_db_by_usage(
user_id=user_id,
db=db_session,
usage="chapter_analysis"
)
# 获取已埋入的伏笔列表(用于回收匹配,传入当前章节号以启用智能标记)
existing_foreshadows = await foreshadow_service.get_planted_foreshadows_for_analysis(
db=db_session,
@@ -2229,8 +2236,7 @@ async def _run_chapter_generation_bg(
chapter_id=chapter_id,
user_id=user_id,
project_id=current_chapter.project_id,
task_id=analysis_task.id,
ai_service=ai_service
task_id=analysis_task.id
)
)
@@ -2441,7 +2447,7 @@ async def _run_batch_analysis_in_sequence(
tasks_queue: list[dict[str, int | str]],
user_id: str,
project_id: str,
ai_service: AIService
ai_service: Optional[AIService] = None
) -> None:
"""按章节顺序逐个执行分析任务。"""
for index, task_item in enumerate(tasks_queue, start=1):
@@ -2477,8 +2483,7 @@ async def batch_analyze_unanalyzed_chapters(
project_id: str,
payload: BatchAnalyzeUnanalyzedRequest,
request: Request,
db: AsyncSession = Depends(get_db),
user_ai_service: AIService = Depends(get_user_ai_service)
db: AsyncSession = Depends(get_db)
):
"""自动识别项目中未完成分析的章节,并按章节顺序逐个启动分析。"""
user_id = getattr(request.state, "user_id", None)
@@ -2585,8 +2590,7 @@ async def batch_analyze_unanalyzed_chapters(
_run_batch_analysis_in_sequence(
tasks_queue=tasks_queue,
user_id=user_id,
project_id=project_id,
ai_service=user_ai_service
project_id=project_id
)
)
@@ -2818,8 +2822,7 @@ async def trigger_chapter_analysis(
chapter_id: str,
request: Request,
background_tasks: BackgroundTasks,
db: AsyncSession = Depends(get_db),
user_ai_service: AIService = Depends(get_user_ai_service)
db: AsyncSession = Depends(get_db)
):
"""
手动触发章节分析(用于重新分析或分析旧章节)
@@ -2876,8 +2879,7 @@ async def trigger_chapter_analysis(
chapter_id=chapter_id,
user_id=user_id,
project_id=project.id,
task_id=task_id,
ai_service=user_ai_service
task_id=task_id
)
return {
@@ -3274,8 +3276,7 @@ async def execute_batch_generation_in_order(
chapter_id=chapter_id,
user_id=user_id,
project_id=task.project_id,
task_id=analysis_task.id,
ai_service=ai_service
task_id=analysis_task.id
)
# 直接根据返回值判断
+118 -3
View File
@@ -19,6 +19,7 @@ from app.schemas.settings import (
SettingsCreate, SettingsUpdate, SettingsResponse,
APIKeyPreset, APIKeyPresetConfig, PresetCreateRequest,
PresetUpdateRequest, PresetResponse, PresetListResponse,
ChapterAnalysisPresetSelectionRequest,
SystemSMTPSettingsResponse, SystemSMTPSettingsUpdate, SMTPTestRequest
)
from app.user_manager import User
@@ -52,6 +53,53 @@ def read_env_defaults() -> Dict[str, Any]:
}
def _safe_load_preferences(raw_preferences: Optional[str]) -> Dict[str, Any]:
"""安全解析用户偏好设置。"""
try:
return json.loads(raw_preferences or '{}')
except (json.JSONDecodeError, TypeError):
return {}
def _get_api_presets_payload(prefs: Dict[str, Any]) -> Dict[str, Any]:
"""获取API预设偏好结构。"""
api_presets = prefs.get('api_presets')
if not isinstance(api_presets, dict):
api_presets = {'presets': [], 'version': '1.0'}
if not isinstance(api_presets.get('presets'), list):
api_presets['presets'] = []
api_presets.setdefault('version', '1.0')
return api_presets
def _get_chapter_analysis_preset_id(prefs: Dict[str, Any]) -> Optional[str]:
"""读取章节内容分析专用API预设ID。"""
preset_id = prefs.get('chapter_analysis_preset_id')
return preset_id if isinstance(preset_id, str) and preset_id.strip() else None
def _build_ai_service_from_config(
*,
config: Dict[str, Any],
user_id: str,
db: AsyncSession,
enable_mcp: bool,
) -> AIService:
"""基于指定配置创建AI服务。"""
return create_user_ai_service_with_mcp(
api_provider=normalize_provider(config.get('api_provider')),
api_key=config.get('api_key') or "",
api_base_url=config.get('api_base_url') or "",
model_name=config.get('llm_model') or app_settings.default_model,
temperature=config.get('temperature') if config.get('temperature') is not None else app_settings.default_temperature,
max_tokens=config.get('max_tokens') if config.get('max_tokens') is not None else app_settings.default_max_tokens,
user_id=user_id,
db_session=db,
system_prompt=config.get('system_prompt'),
enable_mcp=enable_mcp,
)
def require_login(request: Request):
"""依赖:要求用户已登录"""
if not hasattr(request.state, "user") or not request.state.user:
@@ -164,6 +212,15 @@ async def get_user_ai_service_from_db(user_id: str, db: AsyncSession) -> AIServi
"""
从数据库直接创建用户AI服务实例(用于后台任务,不依赖FastAPI的Depends
"""
return await get_user_ai_service_from_db_by_usage(user_id, db, usage="default")
async def get_user_ai_service_from_db_by_usage(
user_id: str,
db: AsyncSession,
usage: str = "default"
) -> AIService:
"""按用途创建用户AI服务实例。"""
from app.models.mcp_plugin import MCPPlugin
result = await db.execute(
@@ -184,6 +241,23 @@ async def get_user_ai_service_from_db(user_id: str, db: AsyncSession) -> AIServi
mcp_plugins = mcp_result.scalars().all()
enable_mcp = any(plugin.enabled for plugin in mcp_plugins) if mcp_plugins else False
if usage == "chapter_analysis":
prefs = _safe_load_preferences(settings.preferences)
api_presets = _get_api_presets_payload(prefs)
presets = api_presets.get('presets', [])
preset_id = _get_chapter_analysis_preset_id(prefs)
if preset_id:
target_preset = next((p for p in presets if p.get('id') == preset_id), None)
if target_preset and isinstance(target_preset.get('config'), dict):
logger.info(f"用户 {user_id} 使用章节内容分析专用API预设: {target_preset.get('name')}")
return _build_ai_service_from_config(
config=target_preset['config'],
user_id=user_id,
db=db,
enable_mcp=enable_mcp,
)
logger.warning(f"用户 {user_id} 配置的章节内容分析预设不存在,回退默认API配置: {preset_id}")
return create_user_ai_service_with_mcp(
api_provider=settings.api_provider,
api_key=settings.api_key,
@@ -1043,8 +1117,11 @@ async def get_presets(
logger.warning(f"用户 {user.user_id} 的preferences字段JSON格式错误,重置为空")
prefs = {}
api_presets = prefs.get('api_presets', {'presets': [], 'version': '1.0'})
api_presets = _get_api_presets_payload(prefs)
presets = api_presets.get('presets', [])
chapter_analysis_preset_id = _get_chapter_analysis_preset_id(prefs)
if chapter_analysis_preset_id and not any(p.get('id') == chapter_analysis_preset_id for p in presets):
chapter_analysis_preset_id = None
# 找到激活的预设
active_preset_id = next(
@@ -1057,7 +1134,8 @@ async def get_presets(
return {
"presets": presets,
"total": len(presets),
"active_preset_id": active_preset_id
"active_preset_id": active_preset_id,
"chapter_analysis_preset_id": chapter_analysis_preset_id
}
@@ -1177,7 +1255,7 @@ async def delete_preset(
except json.JSONDecodeError:
raise HTTPException(status_code=500, detail="配置数据格式错误")
api_presets = prefs.get('api_presets', {'presets': [], 'version': '1.0'})
api_presets = _get_api_presets_payload(prefs)
presets = api_presets.get('presets', [])
# 找到预设
@@ -1191,6 +1269,8 @@ async def delete_preset(
# 删除预设
presets = [p for p in presets if p['id'] != preset_id]
if prefs.get('chapter_analysis_preset_id') == preset_id:
prefs.pop('chapter_analysis_preset_id', None)
# 保存回preferences
api_presets['presets'] = presets
@@ -1258,6 +1338,41 @@ async def activate_preset(
}
@router.put("/presets/usage/chapter-analysis")
async def set_chapter_analysis_preset_selection(
data: ChapterAnalysisPresetSelectionRequest,
user: User = Depends(require_login),
db: AsyncSession = Depends(get_db)
):
"""设置章节内容分析专用API预设;为空则使用默认API配置。"""
settings = await get_user_settings(user.user_id, db)
prefs = _safe_load_preferences(settings.preferences)
api_presets = _get_api_presets_payload(prefs)
presets = api_presets.get('presets', [])
preset_id = data.preset_id.strip() if data.preset_id else None
preset_name = None
if preset_id:
target_preset = next((p for p in presets if p.get('id') == preset_id), None)
if not target_preset:
raise HTTPException(status_code=404, detail="预设不存在")
prefs['chapter_analysis_preset_id'] = preset_id
preset_name = target_preset.get('name')
else:
prefs.pop('chapter_analysis_preset_id', None)
prefs['api_presets'] = api_presets
settings.preferences = json.dumps(prefs, ensure_ascii=False)
await db.commit()
logger.info(f"用户 {user.user_id} 设置章节内容分析API预设: {preset_id or '默认配置'}")
return {
"message": "章节内容分析API配置已更新",
"chapter_analysis_preset_id": preset_id,
"preset_name": preset_name
}
@router.post("/presets/{preset_id}/test")
async def test_preset(
preset_id: str,
+9 -1
View File
@@ -140,4 +140,12 @@ class PresetListResponse(BaseModel):
presets: List[PresetResponse] = Field(..., description="预设列表")
total: int = Field(..., description="总数")
active_preset_id: Optional[str] = Field(None, description="当前激活的预设ID")
active_preset_id: Optional[str] = Field(None, description="当前激活的预设ID")
chapter_analysis_preset_id: Optional[str] = Field(None, description="章节内容分析使用的预设ID,为空则使用默认API配置")
class ChapterAnalysisPresetSelectionRequest(BaseModel):
"""章节内容分析预设选择请求"""
model_config = ConfigDict(protected_namespaces=())
preset_id: Optional[str] = Field(None, description="章节内容分析使用的预设ID;为空则使用默认API配置")