update:1.优化章节管理(1->N)模式下支持修改章节标题 2.AI创作章节内容支持加载模型列表,使用不同模型创作
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
@@ -351,7 +351,7 @@ MuMuAINovel/
|
|||||||
- 提交 [Issue](https://github.com/xiamuceer-j/MuMuAINovel/issues)
|
- 提交 [Issue](https://github.com/xiamuceer-j/MuMuAINovel/issues)
|
||||||
- Linux DO [讨论](https://linux.do/t/topic/1106333)
|
- Linux DO [讨论](https://linux.do/t/topic/1106333)
|
||||||
- 加入QQ群 [QQ群](frontend/public/qq.jpg)
|
- 加入QQ群 [QQ群](frontend/public/qq.jpg)
|
||||||
- 加入WX群 [WX群](frontend/public/WX.jpg)
|
- 加入WX群 [WX群](frontend/public/WX.png)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -953,6 +953,7 @@ async def generate_chapter_content_stream(
|
|||||||
style_id = generate_request.style_id
|
style_id = generate_request.style_id
|
||||||
target_word_count = generate_request.target_word_count or 3000
|
target_word_count = generate_request.target_word_count or 3000
|
||||||
enable_mcp = generate_request.enable_mcp if hasattr(generate_request, 'enable_mcp') else True
|
enable_mcp = generate_request.enable_mcp if hasattr(generate_request, 'enable_mcp') else True
|
||||||
|
custom_model = generate_request.model if hasattr(generate_request, 'model') else None
|
||||||
# 预先验证章节存在性(使用临时会话)
|
# 预先验证章节存在性(使用临时会话)
|
||||||
async for temp_db in get_db(request):
|
async for temp_db in get_db(request):
|
||||||
try:
|
try:
|
||||||
@@ -1310,12 +1311,20 @@ async def generate_chapter_content_stream(
|
|||||||
|
|
||||||
logger.info(f"开始AI流式创作章节 {chapter_id}")
|
logger.info(f"开始AI流式创作章节 {chapter_id}")
|
||||||
|
|
||||||
|
# 准备生成参数
|
||||||
|
generate_kwargs = {"prompt": prompt}
|
||||||
|
if custom_model:
|
||||||
|
logger.info(f" 使用自定义模型: {custom_model}")
|
||||||
|
generate_kwargs["model"] = custom_model
|
||||||
|
# 注意:这里使用用户配置的AI服务,模型参数会覆盖默认模型
|
||||||
|
# 如果需要切换provider,需要在前端传递provider参数
|
||||||
|
|
||||||
# 流式生成内容
|
# 流式生成内容
|
||||||
full_content = ""
|
full_content = ""
|
||||||
chunk_count = 0
|
chunk_count = 0
|
||||||
last_progress = 0
|
last_progress = 0
|
||||||
|
|
||||||
async for chunk in user_ai_service.generate_text_stream(prompt=prompt):
|
async for chunk in user_ai_service.generate_text_stream(**generate_kwargs):
|
||||||
full_content += chunk
|
full_content += chunk
|
||||||
chunk_count += 1
|
chunk_count += 1
|
||||||
|
|
||||||
@@ -2494,7 +2503,10 @@ async def generate_single_chapter_for_batch(
|
|||||||
|
|
||||||
# 非流式生成内容
|
# 非流式生成内容
|
||||||
full_content = ""
|
full_content = ""
|
||||||
async for chunk in ai_service.generate_text_stream(prompt=prompt):
|
async for chunk in ai_service.generate_text_stream(
|
||||||
|
prompt=prompt,
|
||||||
|
model=None # 批量生成时使用用户默认模型,后续可扩展
|
||||||
|
):
|
||||||
full_content += chunk
|
full_content += chunk
|
||||||
|
|
||||||
# 更新章节内容到数据库(使用锁保护)
|
# 更新章节内容到数据库(使用锁保护)
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ class ChapterGenerateRequest(BaseModel):
|
|||||||
le=10000 # 最大10000字
|
le=10000 # 最大10000字
|
||||||
)
|
)
|
||||||
enable_mcp: bool = Field(True, description="是否启用MCP工具增强(搜索参考资料)")
|
enable_mcp: bool = Field(True, description="是否启用MCP工具增强(搜索参考资料)")
|
||||||
|
model: Optional[str] = Field(None, description="指定使用的AI模型,不提供则使用用户默认模型")
|
||||||
|
|
||||||
|
|
||||||
class BatchGenerateRequest(BaseModel):
|
class BatchGenerateRequest(BaseModel):
|
||||||
@@ -94,6 +95,7 @@ class BatchGenerateRequest(BaseModel):
|
|||||||
enable_analysis: bool = Field(False, description="是否启用同步分析")
|
enable_analysis: bool = Field(False, description="是否启用同步分析")
|
||||||
enable_mcp: bool = Field(True, description="是否启用MCP工具增强(搜索参考资料)")
|
enable_mcp: bool = Field(True, description="是否启用MCP工具增强(搜索参考资料)")
|
||||||
max_retries: int = Field(3, description="每个章节的最大重试次数", ge=0, le=5)
|
max_retries: int = Field(3, description="每个章节的最大重试次数", ge=0, le=5)
|
||||||
|
model: Optional[str] = Field(None, description="指定使用的AI模型,不提供则使用用户默认模型")
|
||||||
|
|
||||||
|
|
||||||
class BatchGenerateResponse(BaseModel):
|
class BatchGenerateResponse(BaseModel):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.8",
|
"version": "1.0.9",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export default function Chapters() {
|
|||||||
const [writingStyles, setWritingStyles] = useState<WritingStyle[]>([]);
|
const [writingStyles, setWritingStyles] = useState<WritingStyle[]>([]);
|
||||||
const [selectedStyleId, setSelectedStyleId] = useState<number | undefined>();
|
const [selectedStyleId, setSelectedStyleId] = useState<number | undefined>();
|
||||||
const [targetWordCount, setTargetWordCount] = useState<number>(3000);
|
const [targetWordCount, setTargetWordCount] = useState<number>(3000);
|
||||||
|
const [availableModels, setAvailableModels] = useState<Array<{value: string, label: string}>>([]);
|
||||||
|
const [selectedModel, setSelectedModel] = useState<string | undefined>();
|
||||||
const [analysisVisible, setAnalysisVisible] = useState(false);
|
const [analysisVisible, setAnalysisVisible] = useState(false);
|
||||||
const [analysisChapterId, setAnalysisChapterId] = useState<string | null>(null);
|
const [analysisChapterId, setAnalysisChapterId] = useState<string | null>(null);
|
||||||
// 分析任务状态管理
|
// 分析任务状态管理
|
||||||
@@ -187,6 +189,37 @@ export default function Chapters() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadAvailableModels = async () => {
|
||||||
|
try {
|
||||||
|
// 从设置API获取用户配置的模型列表
|
||||||
|
const settingsResponse = await fetch('/api/settings');
|
||||||
|
if (settingsResponse.ok) {
|
||||||
|
const settings = await settingsResponse.json();
|
||||||
|
const { api_key, api_base_url, api_provider } = settings;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
setAvailableModels(data.models);
|
||||||
|
// 设置默认模型为当前配置的模型
|
||||||
|
setSelectedModel(settings.llm_model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('获取模型列表失败,将使用默认模型');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载可用模型失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 检查并恢复批量生成任务
|
// 检查并恢复批量生成任务
|
||||||
const checkAndRestoreBatchTask = async () => {
|
const checkAndRestoreBatchTask = async () => {
|
||||||
if (!currentProject?.id) return;
|
if (!currentProject?.id) return;
|
||||||
@@ -270,6 +303,10 @@ export default function Chapters() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await updateChapter(editingId, values);
|
await updateChapter(editingId, values);
|
||||||
|
|
||||||
|
// 刷新章节列表以获取完整的章节数据(包括outline_title等联查字段)
|
||||||
|
await refreshChapters();
|
||||||
|
|
||||||
message.success('章节更新成功');
|
message.success('章节更新成功');
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
@@ -288,6 +325,8 @@ export default function Chapters() {
|
|||||||
});
|
});
|
||||||
setEditingId(id);
|
setEditingId(id);
|
||||||
setIsEditorOpen(true);
|
setIsEditorOpen(true);
|
||||||
|
// 打开编辑窗口时加载模型列表
|
||||||
|
loadAvailableModels();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -335,7 +374,8 @@ export default function Chapters() {
|
|||||||
// 进度回调
|
// 进度回调
|
||||||
setSingleChapterProgress(progressValue);
|
setSingleChapterProgress(progressValue);
|
||||||
setSingleChapterProgressMessage(progressMsg);
|
setSingleChapterProgressMessage(progressMsg);
|
||||||
}
|
},
|
||||||
|
selectedModel // 传递选中的模型
|
||||||
);
|
);
|
||||||
|
|
||||||
message.success('AI创作成功,正在分析章节内容...');
|
message.success('AI创作成功,正在分析章节内容...');
|
||||||
@@ -1517,9 +1557,21 @@ export default function Chapters() {
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
label="章节标题"
|
label="章节标题"
|
||||||
name="title"
|
name="title"
|
||||||
tooltip="章节标题由大纲管理,建议在大纲页面统一修改"
|
tooltip={
|
||||||
|
currentProject.outline_mode === 'one-to-one'
|
||||||
|
? "章节标题由大纲管理,建议在大纲页面统一修改"
|
||||||
|
: "一对多模式下可以修改章节标题"
|
||||||
|
}
|
||||||
|
rules={
|
||||||
|
currentProject.outline_mode === 'one-to-many'
|
||||||
|
? [{ required: true, message: '请输入章节标题' }]
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Input placeholder="输入章节标题" disabled />
|
<Input
|
||||||
|
placeholder="输入章节标题"
|
||||||
|
disabled={currentProject.outline_mode === 'one-to-one'}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -1666,6 +1718,32 @@ export default function Chapters() {
|
|||||||
</div>
|
</div>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="AI模型"
|
||||||
|
tooltip="选择用于生成章节内容的AI模型,不选择则使用默认模型"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
placeholder="使用默认模型"
|
||||||
|
value={selectedModel}
|
||||||
|
onChange={setSelectedModel}
|
||||||
|
size="large"
|
||||||
|
allowClear
|
||||||
|
disabled={isGenerating}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
showSearch
|
||||||
|
optionFilterProp="label"
|
||||||
|
>
|
||||||
|
{availableModels.map(model => (
|
||||||
|
<Select.Option key={model.value} value={model.value} label={model.label}>
|
||||||
|
{model.label}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<div style={{ color: '#666', fontSize: 12, marginTop: 4 }}>
|
||||||
|
不同模型有不同的特点和定价,请根据需要选择
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label="章节内容" name="content">
|
<Form.Item label="章节内容" name="content">
|
||||||
<TextArea
|
<TextArea
|
||||||
ref={contentTextAreaRef}
|
ref={contentTextAreaRef}
|
||||||
|
|||||||
@@ -286,7 +286,8 @@ export function useChapterSync() {
|
|||||||
onProgress?: (content: string) => void,
|
onProgress?: (content: string) => void,
|
||||||
styleId?: number,
|
styleId?: number,
|
||||||
targetWordCount?: number,
|
targetWordCount?: number,
|
||||||
onProgressUpdate?: (message: string, progress: number) => void
|
onProgressUpdate?: (message: string, progress: number) => void,
|
||||||
|
model?: string
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
// 使用fetch处理流式响应
|
// 使用fetch处理流式响应
|
||||||
@@ -297,7 +298,8 @@ export function useChapterSync() {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
style_id: styleId,
|
style_id: styleId,
|
||||||
target_word_count: targetWordCount
|
target_word_count: targetWordCount,
|
||||||
|
model: model
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user