update:1.增加AI批量创作章节内容时,支持加载模型列表,使用不同模型批量生成内容 2.支持AI生成/续写大纲时,选择自定义模型进行生成
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
+18
-10
@@ -1973,12 +1973,13 @@ async def batch_generate_chapters_in_order(
|
||||
|
||||
logger.info(f"📦 创建批量生成任务: {batch_id}, 章节: 第{start_number}-{end_number}章, 预估耗时: {estimated_time}分钟")
|
||||
|
||||
# 启动后台批量生成任务
|
||||
# 启动后台批量生成任务,传递model参数
|
||||
background_tasks.add_task(
|
||||
execute_batch_generation_in_order,
|
||||
batch_id=batch_id,
|
||||
user_id=user_id,
|
||||
ai_service=user_ai_service
|
||||
ai_service=user_ai_service,
|
||||
custom_model=batch_request.model
|
||||
)
|
||||
|
||||
return BatchGenerateResponse(
|
||||
@@ -2105,7 +2106,8 @@ async def cancel_batch_generation(
|
||||
async def execute_batch_generation_in_order(
|
||||
batch_id: str,
|
||||
user_id: str,
|
||||
ai_service: AIService
|
||||
ai_service: AIService,
|
||||
custom_model: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
按顺序执行批量生成任务(后台任务)
|
||||
@@ -2194,7 +2196,7 @@ async def execute_batch_generation_in_order(
|
||||
if not can_generate:
|
||||
raise Exception(f"前置条件不满足: {error_msg}")
|
||||
|
||||
# 生成章节内容(复用现有流式生成逻辑的核心部分)
|
||||
# 生成章节内容(复用现有流式生成逻辑的核心部分),传递model参数
|
||||
await generate_single_chapter_for_batch(
|
||||
db_session=db_session,
|
||||
chapter=chapter,
|
||||
@@ -2202,7 +2204,8 @@ async def execute_batch_generation_in_order(
|
||||
style_id=task.style_id,
|
||||
target_word_count=task.target_word_count,
|
||||
ai_service=ai_service,
|
||||
write_lock=write_lock
|
||||
write_lock=write_lock,
|
||||
custom_model=custom_model
|
||||
)
|
||||
|
||||
logger.info(f"✅ 章节生成完成: 第{chapter.chapter_number}章")
|
||||
@@ -2316,7 +2319,8 @@ async def generate_single_chapter_for_batch(
|
||||
style_id: Optional[int],
|
||||
target_word_count: int,
|
||||
ai_service: AIService,
|
||||
write_lock: Lock
|
||||
write_lock: Lock,
|
||||
custom_model: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
为批量生成执行单个章节的生成(非流式)
|
||||
@@ -2503,10 +2507,14 @@ async def generate_single_chapter_for_batch(
|
||||
|
||||
# 非流式生成内容
|
||||
full_content = ""
|
||||
async for chunk in ai_service.generate_text_stream(
|
||||
prompt=prompt,
|
||||
model=None # 批量生成时使用用户默认模型,后续可扩展
|
||||
):
|
||||
# 准备生成参数
|
||||
generate_kwargs = {"prompt": prompt}
|
||||
# 如果传入了自定义模型,使用指定的模型
|
||||
if custom_model:
|
||||
generate_kwargs["model"] = custom_model
|
||||
logger.info(f" 批量生成使用自定义模型: {custom_model}")
|
||||
|
||||
async for chunk in ai_service.generate_text_stream(**generate_kwargs):
|
||||
full_content += chunk
|
||||
|
||||
# 更新章节内容到数据库(使用锁保护)
|
||||
|
||||
@@ -1111,10 +1111,19 @@ async def new_outline_generator(
|
||||
|
||||
# 调用AI
|
||||
yield await SSEResponse.send_progress("🤖 正在调用AI生成...", 30)
|
||||
|
||||
# 添加调试日志
|
||||
model_param = data.get("model")
|
||||
provider_param = data.get("provider")
|
||||
logger.info(f"=== 大纲生成AI调用参数 ===")
|
||||
logger.info(f" provider参数: {provider_param}")
|
||||
logger.info(f" model参数: {model_param}")
|
||||
logger.info(f" 完整data: {data}")
|
||||
|
||||
ai_response = await user_ai_service.generate_text(
|
||||
prompt=prompt,
|
||||
provider=data.get("provider"),
|
||||
model=data.get("model")
|
||||
provider=provider_param,
|
||||
model=model_param
|
||||
)
|
||||
|
||||
yield await SSEResponse.send_progress("✅ AI生成完成,正在解析...", 70)
|
||||
@@ -1446,10 +1455,16 @@ async def continue_outline_generator(
|
||||
)
|
||||
|
||||
# 调用AI生成当前批次
|
||||
model_param = data.get("model")
|
||||
provider_param = data.get("provider")
|
||||
logger.info(f"=== 续写批次{batch_num + 1} AI调用参数 ===")
|
||||
logger.info(f" provider参数: {provider_param}")
|
||||
logger.info(f" model参数: {model_param}")
|
||||
|
||||
ai_response = await user_ai_service.generate_text(
|
||||
prompt=prompt,
|
||||
provider=data.get("provider"),
|
||||
model=data.get("model")
|
||||
provider=provider_param,
|
||||
model=model_param
|
||||
)
|
||||
|
||||
yield await SSEResponse.send_progress(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "1.0.9",
|
||||
"version": "1.0.10",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -29,6 +29,7 @@ export default function Chapters() {
|
||||
const [targetWordCount, setTargetWordCount] = useState<number>(3000);
|
||||
const [availableModels, setAvailableModels] = useState<Array<{value: string, label: string}>>([]);
|
||||
const [selectedModel, setSelectedModel] = useState<string | undefined>();
|
||||
const [batchSelectedModel, setBatchSelectedModel] = useState<string | undefined>(); // 批量生成的模型选择
|
||||
const [analysisVisible, setAnalysisVisible] = useState(false);
|
||||
const [analysisChapterId, setAnalysisChapterId] = useState<string | null>(null);
|
||||
// 分析任务状态管理
|
||||
@@ -48,6 +49,7 @@ export default function Chapters() {
|
||||
const [batchGenerateVisible, setBatchGenerateVisible] = useState(false);
|
||||
const [batchGenerating, setBatchGenerating] = useState(false);
|
||||
const [batchTaskId, setBatchTaskId] = useState<string | null>(null);
|
||||
const [batchForm] = Form.useForm();
|
||||
const [batchProgress, setBatchProgress] = useState<{
|
||||
status: string;
|
||||
total: number;
|
||||
@@ -208,6 +210,7 @@ export default function Chapters() {
|
||||
setAvailableModels(data.models);
|
||||
// 设置默认模型为当前配置的模型
|
||||
setSelectedModel(settings.llm_model);
|
||||
return settings.llm_model; // 返回模型名称
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -218,6 +221,7 @@ export default function Chapters() {
|
||||
} catch (error) {
|
||||
console.error('加载可用模型失败:', error);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 检查并恢复批量生成任务
|
||||
@@ -590,13 +594,23 @@ export default function Chapters() {
|
||||
enableAnalysis: boolean;
|
||||
styleId?: number;
|
||||
targetWordCount?: number;
|
||||
model?: string;
|
||||
}) => {
|
||||
if (!currentProject?.id) return;
|
||||
|
||||
// 调试日志
|
||||
console.log('[批量生成] 表单values:', values);
|
||||
console.log('[批量生成] batchSelectedModel状态:', batchSelectedModel);
|
||||
|
||||
// 使用批量生成对话框中选择的风格和字数,如果没有选择则使用默认值
|
||||
const styleId = values.styleId || selectedStyleId;
|
||||
const wordCount = values.targetWordCount || targetWordCount;
|
||||
|
||||
// 使用批量生成专用的模型状态
|
||||
const model = batchSelectedModel;
|
||||
|
||||
console.log('[批量生成] 最终使用的model:', model);
|
||||
|
||||
if (!styleId) {
|
||||
message.error('请选择写作风格');
|
||||
return;
|
||||
@@ -606,18 +620,30 @@ export default function Chapters() {
|
||||
setBatchGenerating(true);
|
||||
setBatchGenerateVisible(false); // 关闭配置对话框,避免遮挡进度弹窗
|
||||
|
||||
const response = await fetch(`/api/chapters/project/${currentProject.id}/batch-generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
const requestBody: any = {
|
||||
start_chapter_number: values.startChapterNumber,
|
||||
count: values.count,
|
||||
enable_analysis: values.enableAnalysis,
|
||||
style_id: styleId,
|
||||
target_word_count: wordCount,
|
||||
}),
|
||||
};
|
||||
|
||||
// 如果有模型参数,添加到请求体中
|
||||
if (model) {
|
||||
requestBody.model = model;
|
||||
console.log('[批量生成] 请求体包含model:', model);
|
||||
} else {
|
||||
console.log('[批量生成] 请求体不包含model,使用后端默认模型');
|
||||
}
|
||||
|
||||
console.log('[批量生成] 完整请求体:', JSON.stringify(requestBody, null, 2));
|
||||
|
||||
const response = await fetch(`/api/chapters/project/${currentProject.id}/batch-generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -725,7 +751,7 @@ export default function Chapters() {
|
||||
};
|
||||
|
||||
// 打开批量生成对话框
|
||||
const handleOpenBatchGenerate = () => {
|
||||
const handleOpenBatchGenerate = async () => {
|
||||
// 找到第一个未生成的章节
|
||||
const firstIncompleteChapter = sortedChapters.find(
|
||||
ch => !ch.content || ch.content.trim() === ''
|
||||
@@ -743,6 +769,24 @@ export default function Chapters() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 打开对话框时加载模型列表,等待完成
|
||||
const defaultModel = await loadAvailableModels();
|
||||
|
||||
console.log('[打开批量生成] defaultModel:', defaultModel);
|
||||
console.log('[打开批量生成] selectedStyleId:', selectedStyleId);
|
||||
|
||||
// 设置批量生成的模型选择状态
|
||||
setBatchSelectedModel(defaultModel || undefined);
|
||||
|
||||
// 重置表单并设置初始值
|
||||
batchForm.setFieldsValue({
|
||||
startChapterNumber: firstIncompleteChapter.chapter_number,
|
||||
count: 5,
|
||||
enableAnalysis: false,
|
||||
styleId: selectedStyleId,
|
||||
targetWordCount: 3000,
|
||||
});
|
||||
|
||||
setBatchGenerateVisible(true);
|
||||
};
|
||||
|
||||
@@ -1723,7 +1767,7 @@ export default function Chapters() {
|
||||
tooltip="选择用于生成章节内容的AI模型,不选择则使用默认模型"
|
||||
>
|
||||
<Select
|
||||
placeholder="使用默认模型"
|
||||
placeholder={selectedModel ? `默认: ${availableModels.find(m => m.value === selectedModel)?.label || selectedModel}` : "使用默认模型"}
|
||||
value={selectedModel}
|
||||
onChange={setSelectedModel}
|
||||
size="large"
|
||||
@@ -1736,11 +1780,12 @@ export default function Chapters() {
|
||||
{availableModels.map(model => (
|
||||
<Select.Option key={model.value} value={model.value} label={model.label}>
|
||||
{model.label}
|
||||
{model.value === selectedModel && ' (默认)'}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<div style={{ color: '#666', fontSize: 12, marginTop: 4 }}>
|
||||
不同模型有不同的特点和定价,请根据需要选择
|
||||
{selectedModel ? `当前默认模型: ${availableModels.find(m => m.value === selectedModel)?.label || selectedModel}` : '加载模型列表中...'}
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
@@ -1889,6 +1934,7 @@ export default function Chapters() {
|
||||
>
|
||||
{!batchGenerating ? (
|
||||
<Form
|
||||
form={batchForm}
|
||||
layout="vertical"
|
||||
onFinish={handleBatchGenerate}
|
||||
initialValues={{
|
||||
@@ -1897,6 +1943,7 @@ export default function Chapters() {
|
||||
enableAnalysis: false,
|
||||
styleId: selectedStyleId,
|
||||
targetWordCount: 3000,
|
||||
model: selectedModel,
|
||||
}}
|
||||
>
|
||||
<Alert
|
||||
@@ -1989,6 +2036,31 @@ export default function Chapters() {
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="AI模型"
|
||||
tooltip="批量生成时所有章节使用相同模型,不选择则使用默认模型"
|
||||
>
|
||||
<Select
|
||||
placeholder={batchSelectedModel ? `默认: ${availableModels.find(m => m.value === batchSelectedModel)?.label || batchSelectedModel}` : "使用默认模型"}
|
||||
value={batchSelectedModel}
|
||||
onChange={setBatchSelectedModel}
|
||||
size="large"
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
>
|
||||
{availableModels.map(model => (
|
||||
<Select.Option key={model.value} value={model.value} label={model.label}>
|
||||
{model.label}
|
||||
{model.value === batchSelectedModel && ' (默认)'}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<div style={{ color: '#666', fontSize: 12, marginTop: 4 }}>
|
||||
{batchSelectedModel ? `当前默认模型: ${availableModels.find(m => m.value === batchSelectedModel)?.label || batchSelectedModel}` : '加载模型列表中...'}
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="同步分析"
|
||||
name="enableAnalysis"
|
||||
|
||||
@@ -165,6 +165,12 @@ export default function Outline() {
|
||||
try {
|
||||
setIsGenerating(true);
|
||||
|
||||
// 添加详细的调试日志
|
||||
console.log('=== 大纲生成调试信息 ===');
|
||||
console.log('1. Form values 原始数据:', values);
|
||||
console.log('2. values.model:', values.model);
|
||||
console.log('3. values.provider:', values.provider);
|
||||
|
||||
// 关闭生成表单Modal
|
||||
Modal.destroyAll();
|
||||
|
||||
@@ -174,7 +180,7 @@ export default function Outline() {
|
||||
setSSEModalVisible(true);
|
||||
|
||||
// 准备请求数据
|
||||
const requestData = {
|
||||
const requestData: any = {
|
||||
project_id: currentProject.id,
|
||||
genre: currentProject.genre || '通用',
|
||||
theme: values.theme || currentProject.theme || '',
|
||||
@@ -184,11 +190,26 @@ export default function Outline() {
|
||||
requirements: values.requirements,
|
||||
mode: values.mode || 'auto',
|
||||
story_direction: values.story_direction,
|
||||
plot_stage: values.plot_stage || 'development',
|
||||
provider: values.provider,
|
||||
model: values.model
|
||||
plot_stage: values.plot_stage || 'development'
|
||||
};
|
||||
|
||||
// 只有在用户选择了模型时才添加model参数
|
||||
if (values.model) {
|
||||
requestData.model = values.model;
|
||||
console.log('4. 添加model到请求:', values.model);
|
||||
} else {
|
||||
console.log('4. values.model为空,不添加到请求');
|
||||
}
|
||||
|
||||
// 添加provider参数(如果有)
|
||||
if (values.provider) {
|
||||
requestData.provider = values.provider;
|
||||
console.log('5. 添加provider到请求:', values.provider);
|
||||
}
|
||||
|
||||
console.log('6. 最终请求数据:', JSON.stringify(requestData, null, 2));
|
||||
console.log('=========================');
|
||||
|
||||
// 使用SSE客户端
|
||||
const apiUrl = `/api/outlines/generate-stream`;
|
||||
const client = new SSEPostClient(apiUrl, requestData, {
|
||||
@@ -224,10 +245,35 @@ export default function Outline() {
|
||||
}
|
||||
};
|
||||
|
||||
const showGenerateModal = () => {
|
||||
const showGenerateModal = async () => {
|
||||
const hasOutlines = outlines.length > 0;
|
||||
const initialMode = hasOutlines ? 'continue' : 'new';
|
||||
|
||||
// 直接加载可用模型列表
|
||||
const settingsResponse = await fetch('/api/settings');
|
||||
const settings = await settingsResponse.json();
|
||||
const { api_key, api_base_url, api_provider } = settings;
|
||||
|
||||
let loadedModels: Array<{value: string, label: string}> = [];
|
||||
let defaultModel: string | undefined = undefined;
|
||||
|
||||
if (api_key && api_base_url) {
|
||||
try {
|
||||
const modelsResponse = await fetch(
|
||||
`/api/settings/models?api_key=${encodeURIComponent(api_key)}&api_base_url=${encodeURIComponent(api_base_url)}&provider=${api_provider}`
|
||||
);
|
||||
if (modelsResponse.ok) {
|
||||
const data = await modelsResponse.json();
|
||||
if (data.models && data.models.length > 0) {
|
||||
loadedModels = data.models;
|
||||
defaultModel = settings.llm_model;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('获取模型列表失败,将使用默认模型');
|
||||
}
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
title: hasOutlines ? (
|
||||
<Space>
|
||||
@@ -249,6 +295,7 @@ export default function Outline() {
|
||||
plot_stage: 'development',
|
||||
keep_existing: true,
|
||||
theme: currentProject.theme || '',
|
||||
model: defaultModel, // 添加默认模型
|
||||
}}
|
||||
>
|
||||
{hasOutlines && (
|
||||
@@ -360,6 +407,32 @@ export default function Outline() {
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
{/* 自定义模型选择 - 移到外层,所有模式都显示 */}
|
||||
{loadedModels.length > 0 && (
|
||||
<Form.Item
|
||||
label="AI模型"
|
||||
name="model"
|
||||
tooltip="选择用于生成的AI模型,不选则使用系统默认模型"
|
||||
>
|
||||
<Select
|
||||
placeholder={defaultModel ? `默认: ${loadedModels.find(m => m.value === defaultModel)?.label || defaultModel}` : "使用默认模型"}
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
options={loadedModels}
|
||||
onChange={(value) => {
|
||||
console.log('用户在下拉框中选择了模型:', value);
|
||||
// 手动同步到Form
|
||||
generateForm.setFieldsValue({ model: value });
|
||||
console.log('已同步到Form,当前Form值:', generateForm.getFieldsValue());
|
||||
}}
|
||||
/>
|
||||
<div style={{ color: '#666', fontSize: 12, marginTop: 4 }}>
|
||||
{defaultModel ? `当前默认模型: ${loadedModels.find(m => m.value === defaultModel)?.label || defaultModel}` : '未配置默认模型'}
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
),
|
||||
okText: hasOutlines ? '开始续写' : '开始生成',
|
||||
|
||||
Reference in New Issue
Block a user