refactor: 后端代码重构,提取通用权限验证逻辑至common模块,减少代码冗余

This commit is contained in:
xiamuceer-j
2026-01-13 16:45:58 +08:00
parent 6f33e12ead
commit 46debab624
14 changed files with 907 additions and 716 deletions
+22 -1
View File
@@ -326,8 +326,29 @@ body {
font-size: 22px !important;
}
/* 折叠状态下隐藏文字但保持点击区域 */
.modern-sider.ant-layout-sider-collapsed .ant-menu-item .ant-menu-title-content {
display: none !important;
width: 0 !important;
overflow: hidden !important;
opacity: 0 !important;
margin: 0 !important;
padding: 0 !important;
}
/* 确保折叠状态下的 Link 覆盖整个菜单项区域 */
.modern-sider.ant-layout-sider-collapsed .ant-menu-item {
position: relative;
}
.modern-sider.ant-layout-sider-collapsed .ant-menu-item a {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
}
/* 选中项左侧指示条 */
+178 -178
View File
@@ -5,6 +5,7 @@ import { useStore } from '../store';
import { useChapterSync } from '../store/hooks';
import { projectApi, writingStyleApi, chapterApi } from '../services/api';
import type { Chapter, ChapterUpdate, ApiError, WritingStyle, AnalysisTask, ExpansionPlanData } from '../types';
import type { TextAreaRef } from 'antd/es/input/TextArea';
import ChapterAnalysis from '../components/ChapterAnalysis';
import ExpansionPlanEditor from '../components/ExpansionPlanEditor';
import { SSELoadingOverlay } from '../components/SSELoadingOverlay';
@@ -54,7 +55,7 @@ export default function Chapters() {
const [form] = Form.useForm();
const [editorForm] = Form.useForm();
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
const contentTextAreaRef = useRef<any>(null);
const contentTextAreaRef = useRef<TextAreaRef>(null);
const [writingStyles, setWritingStyles] = useState<WritingStyle[]>([]);
const [selectedStyleId, setSelectedStyleId] = useState<number | undefined>();
const [targetWordCount, setTargetWordCount] = useState<number>(getCachedWordCount);
@@ -124,12 +125,14 @@ export default function Chapters() {
// 清理轮询定时器
useEffect(() => {
const pollingIntervals = pollingIntervalsRef.current;
const batchPollingInterval = batchPollingIntervalRef.current;
return () => {
Object.values(pollingIntervalsRef.current).forEach(interval => {
Object.values(pollingIntervals).forEach(interval => {
clearInterval(interval);
});
if (batchPollingIntervalRef.current) {
clearInterval(batchPollingIntervalRef.current);
if (batchPollingInterval) {
clearInterval(batchPollingInterval);
}
};
}, []);
@@ -156,7 +159,7 @@ export default function Chapters() {
startPollingTask(chapter.id);
}
}
} catch (error) {
} catch {
// 404或其他错误表示没有分析任务,忽略
console.debug(`章节 ${chapter.id} 暂无分析任务`);
}
@@ -252,7 +255,7 @@ export default function Chapters() {
return settings.llm_model; // 返回模型名称
}
}
} catch (error) {
} catch {
console.log('获取模型列表失败,将使用默认模型');
}
}
@@ -339,6 +342,38 @@ export default function Chapters() {
}
};
// 按章节号排序并按大纲分组章节 (必须在早返回之前调用,避免违反 Hooks 规则)
const { sortedChapters, groupedChapters } = useMemo(() => {
const sorted = [...chapters].sort((a, b) => a.chapter_number - b.chapter_number);
const groups: Record<string, {
outlineId: string | null;
outlineTitle: string;
outlineOrder: number;
chapters: Chapter[];
}> = {};
sorted.forEach(chapter => {
const key = chapter.outline_id || 'uncategorized';
if (!groups[key]) {
groups[key] = {
outlineId: chapter.outline_id || null,
outlineTitle: chapter.outline_title || '未分类章节',
outlineOrder: chapter.outline_order ?? 999,
chapters: []
};
}
groups[key].chapters.push(chapter);
});
// 转换为数组并按大纲顺序排序
const grouped = Object.values(groups).sort((a, b) => a.outlineOrder - b.outlineOrder);
return { sortedChapters: sorted, groupedChapters: grouped };
}, [chapters]);
if (!currentProject) return null;
// 获取人称的中文显示文本
@@ -633,7 +668,7 @@ export default function Chapters() {
}
await handleGenerate();
instance.destroy();
} catch (error) {
} catch {
instance.update({
okButtonProps: { danger: true, loading: false },
cancelButtonProps: { disabled: false },
@@ -670,36 +705,6 @@ export default function Chapters() {
return texts[status] || status;
};
const sortedChapters = [...chapters].sort((a, b) => a.chapter_number - b.chapter_number);
// 按大纲分组章节
const groupedChapters = useMemo(() => {
const groups: Record<string, {
outlineId: string | null;
outlineTitle: string;
outlineOrder: number;
chapters: Chapter[];
}> = {};
sortedChapters.forEach(chapter => {
const key = chapter.outline_id || 'uncategorized';
if (!groups[key]) {
groups[key] = {
outlineId: chapter.outline_id || null,
outlineTitle: chapter.outline_title || '未分类章节',
outlineOrder: chapter.outline_order ?? 999,
chapters: []
};
}
groups[key].chapters.push(chapter);
});
// 转换为数组并按大纲顺序排序
return Object.values(groups).sort((a, b) => a.outlineOrder - b.outlineOrder);
}, [sortedChapters]);
const handleExport = () => {
if (chapters.length === 0) {
message.warning('当前项目没有章节,无法导出');
@@ -761,7 +766,14 @@ export default function Chapters() {
setBatchGenerating(true);
setBatchGenerateVisible(false); // 关闭配置对话框,避免遮挡进度弹窗
const requestBody: any = {
const requestBody: {
start_chapter_number: number;
count: number;
enable_analysis: boolean;
style_id: number;
target_word_count: number;
model?: string;
} = {
start_chapter_number: values.startChapterNumber,
count: values.count,
enable_analysis: true,
@@ -814,8 +826,9 @@ export default function Chapters() {
// 开始轮询任务状态
startBatchPolling(result.batch_id);
} catch (error: any) {
message.error('创建批量生成任务失败:' + (error.message || '未知错误'));
} catch (error: unknown) {
const err = error as Error;
message.error('创建批量生成任务失败:' + (err.message || '未知错误'));
setBatchGenerating(false);
setBatchGenerateVisible(false);
}
@@ -936,8 +949,9 @@ export default function Chapters() {
const updatedProject = await projectApi.getProject(currentProject.id);
setCurrentProject(updatedProject);
}
} catch (error: any) {
message.error('取消失败:' + (error.message || '未知错误'));
} catch (error: unknown) {
const err = error as Error;
message.error('取消失败:' + (err.message || '未知错误'));
}
};
@@ -1129,8 +1143,9 @@ export default function Chapters() {
setCurrentProject(updatedProject);
manualCreateForm.resetFields();
} catch (error: any) {
message.error('操作失败:' + (error.message || '未知错误'));
} catch (error: unknown) {
const err = error as Error;
message.error('操作失败:' + (err.message || '未知错误'));
throw error;
}
}
@@ -1154,8 +1169,9 @@ export default function Chapters() {
setCurrentProject(updatedProject);
manualCreateForm.resetFields();
} catch (error: any) {
message.error('创建失败:' + (error.message || '未知错误'));
} catch (error: unknown) {
const err = error as Error;
message.error('创建失败:' + (err.message || '未知错误'));
throw error;
}
}
@@ -1440,8 +1456,9 @@ export default function Chapters() {
}
message.success('章节删除成功');
} catch (error: any) {
message.error('删除章节失败:' + (error.message || '未知错误'));
} catch (error: unknown) {
const err = error as Error;
message.error('删除章节失败:' + (err.message || '未知错误'));
}
};
@@ -1478,8 +1495,9 @@ export default function Chapters() {
// 关闭编辑器
setPlanEditorVisible(false);
setEditingPlanChapter(null);
} catch (error: any) {
message.error('保存规划失败:' + (error.message || '未知错误'));
} catch (error: unknown) {
const err = error as Error;
message.error('保存规划失败:' + (err.message || '未知错误'));
throw error;
}
};
@@ -2193,7 +2211,7 @@ export default function Chapters() {
disabled={isGenerating}
style={{ width: '100%' }}
formatter={(value) => `${value}`}
parser={(value) => value?.replace(' 字', '') as any}
parser={(value) => parseInt(value?.replace(' 字', '') || '0', 10) as unknown as 500}
/>
</Form.Item>
@@ -2357,11 +2375,27 @@ export default function Chapters() {
setBatchGenerateVisible(false);
}
}}
footer={null}
width={600}
footer={!batchGenerating ? (
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => setBatchGenerateVisible(false)}>
</Button>
<Button type="primary" icon={<RocketOutlined />} onClick={() => batchForm.submit()}>
</Button>
</Space>
) : null}
width={700}
centered
closable={!batchGenerating}
maskClosable={!batchGenerating}
styles={{
body: {
maxHeight: 'calc(100vh - 260px)',
overflowY: 'auto',
overflowX: 'hidden'
}
}}
>
{!batchGenerating ? (
<Form
@@ -2371,95 +2405,85 @@ export default function Chapters() {
initialValues={{
startChapterNumber: sortedChapters.find(ch => !ch.content || ch.content.trim() === '')?.chapter_number || 1,
count: 5,
enableAnalysis: true, // 强制启用同步分析
enableAnalysis: true,
styleId: selectedStyleId,
targetWordCount: getCachedWordCount(),
model: selectedModel,
}}
>
<Alert
message="批量生成说明"
description={
<ul style={{ margin: '8px 0 0 0', paddingLeft: 20 }}>
<li></li>
<li>使</li>
<li></li>
</ul>
}
message="批量生成说明:严格按序生成 | 统一风格字数 | 任一失败则终止"
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
<Form.Item
label="起始章节"
name="startChapterNumber"
rules={[{ required: true, message: '请选择起始章节' }]}
>
<Select placeholder="选择起始章节" size="large">
{sortedChapters
.filter(ch => !ch.content || ch.content.trim() === '')
.filter(ch => canGenerateChapter(ch))
.map(ch => (
<Select.Option key={ch.id} value={ch.chapter_number}>
{ch.chapter_number}{ch.title}
{/* 第一行:起始章节 + 生成数量 */}
<div style={{ display: 'flex', gap: 16 }}>
<Form.Item
label="起始章节"
name="startChapterNumber"
rules={[{ required: true, message: '请选择' }]}
style={{ flex: 1, marginBottom: 12 }}
>
<Select placeholder="选择起始章节">
{sortedChapters
.filter(ch => !ch.content || ch.content.trim() === '')
.filter(ch => canGenerateChapter(ch))
.map(ch => (
<Select.Option key={ch.id} value={ch.chapter_number}>
{ch.chapter_number}{ch.title}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label="生成数量"
name="count"
rules={[{ required: true, message: '请选择' }]}
style={{ marginBottom: 12 }}
>
<Radio.Group buttonStyle="solid">
<Radio.Button value={5}>5</Radio.Button>
<Radio.Button value={10}>10</Radio.Button>
<Radio.Button value={15}>15</Radio.Button>
<Radio.Button value={20}>20</Radio.Button>
</Radio.Group>
</Form.Item>
</div>
{/* 第二行:写作风格 + 目标字数 */}
<div style={{ display: 'flex', gap: 16 }}>
<Form.Item
label="写作风格"
name="styleId"
rules={[{ required: true, message: '请选择' }]}
style={{ flex: 1, marginBottom: 12 }}
>
<Select placeholder="请选择写作风格" showSearch optionFilterProp="children">
{writingStyles.map(style => (
<Select.Option key={style.id} value={style.id}>
{style.name}{style.is_default && ' (默认)'}
</Select.Option>
))}
</Select>
</Form.Item>
</Select>
</Form.Item>
<Form.Item
label="生成数量"
name="count"
rules={[{ required: true, message: '请选择生成数量' }]}
>
<Radio.Group buttonStyle="solid" size="large">
<Radio.Button value={5}>5</Radio.Button>
<Radio.Button value={10}>10</Radio.Button>
<Radio.Button value={15}>15</Radio.Button>
<Radio.Button value={20}>20</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item
label="写作风格"
name="styleId"
rules={[{ required: true, message: '请选择写作风格' }]}
tooltip="批量生成时所有章节使用相同的写作风格"
>
<Select
placeholder="请选择写作风格"
size="large"
showSearch
optionFilterProp="children"
>
{writingStyles.map(style => (
<Select.Option key={style.id} value={style.id}>
{style.name}
{style.is_default && ' (默认)'}
{style.description && ` - ${style.description}`}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label="目标字数"
tooltip="AI生成章节时的目标字数,实际生成字数可能略有偏差(修改后会自动记住)"
>
<Form.Item
label="目标字数"
name="targetWordCount"
rules={[{ required: true, message: '请设置目标字数' }]}
noStyle
rules={[{ required: true, message: '请设置' }]}
tooltip="修改后自动记住"
style={{ flex: 1, marginBottom: 12 }}
>
<InputNumber
min={500}
max={10000}
step={100}
size="large"
style={{ width: '100%' }}
formatter={(value) => `${value}`}
parser={(value) => value?.replace(' 字', '') as any}
parser={(value) => parseInt(value?.replace(' 字', '') || '0', 10) as unknown as 500}
onChange={(value) => {
if (value) {
setCachedWordCount(value);
@@ -2467,68 +2491,44 @@ export default function Chapters() {
}}
/>
</Form.Item>
<div style={{ color: '#666', fontSize: 12, marginTop: 4 }}>
500-10000
</div>
</Form.Item>
</div>
<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"
{/* 第三行:AI模型 + 同步分析 */}
<div style={{ display: 'flex', gap: 16 }}>
<Form.Item
label="AI模型"
tooltip="不选则使用默认模型"
style={{ flex: 1, marginBottom: 12 }}
>
{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>
<Select
placeholder={batchSelectedModel ? `默认: ${availableModels.find(m => m.value === batchSelectedModel)?.label || batchSelectedModel}` : "使用默认模型"}
value={batchSelectedModel}
onChange={setBatchSelectedModel}
allowClear
showSearch
optionFilterProp="label"
>
{availableModels.map(model => (
<Select.Option key={model.value} value={model.value} label={model.label}>
{model.label}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label="同步分析"
name="enableAnalysis"
tooltip="批量生成必须开启同步分析,确保角色职业信息和剧情状态的连贯"
>
<Radio.Group disabled>
<Radio value={true}>
<Space direction="vertical" size={0}>
<span style={{ fontSize: 12, color: '#52c41a' }}>
</span>
<span style={{ fontSize: 12, color: '#52c41a' }}>
</span>
<span style={{ fontSize: 12, color: '#ff9800' }}>
50%
</span>
</Space>
</Radio>
</Radio.Group>
</Form.Item>
<Form.Item>
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => setBatchGenerateVisible(false)}>
</Button>
<Button type="primary" htmlType="submit" icon={<RocketOutlined />}>
</Button>
</Space>
</Form.Item>
<Form.Item
label="同步分析"
name="enableAnalysis"
tooltip="必须开启,确保剧情连贯"
style={{ marginBottom: 12 }}
>
<Radio.Group disabled>
<Radio value={true}>
<span style={{ fontSize: 12, color: '#52c41a' }}> </span>
</Radio>
</Radio.Group>
</Form.Item>
</div>
</Form>
) : (
<div>
File diff suppressed because it is too large Load Diff
+79 -35
View File
@@ -7,7 +7,7 @@ import { cardStyles } from '../components/CardStyles';
import { SSEPostClient } from '../utils/sseClient';
import { SSEProgressModal } from '../components/SSEProgressModal';
import { outlineApi, chapterApi, projectApi } from '../services/api';
import type { OutlineExpansionResponse, BatchOutlineExpansionResponse } from '../types';
import type { OutlineExpansionResponse, BatchOutlineExpansionResponse, ChapterPlanItem, ApiError } from '../types';
// 角色预测数据类型
interface PredictedCharacter {
@@ -64,6 +64,42 @@ interface OrganizationConfirmationData {
chapter_range: string;
}
// 大纲生成请求数据类型
interface OutlineGenerateRequestData {
project_id: string;
genre: string;
theme: string;
chapter_count: number;
narrative_perspective: string;
target_words: number;
requirements?: string;
mode: 'auto' | 'new' | 'continue';
story_direction?: string;
plot_stage: 'development' | 'climax' | 'ending';
enable_auto_characters: boolean;
require_character_confirmation: boolean;
enable_auto_organizations: boolean;
require_organization_confirmation: boolean;
model?: string;
provider?: string;
confirmed_characters?: PredictedCharacter[];
confirmed_organizations?: PredictedOrganization[];
}
// 跳过的大纲信息类型
interface SkippedOutlineInfo {
outline_id: string;
outline_title: string;
reason: string;
}
// 场景类型
interface SceneInfo {
location: string;
characters: string[];
purpose: string;
}
const { TextArea } = Input;
export default function Outline() {
@@ -84,7 +120,7 @@ export default function Outline() {
// 角色确认相关状态
const [characterConfirmData, setCharacterConfirmData] = useState<CharacterConfirmationData | null>(null);
const [characterConfirmVisible, setCharacterConfirmVisible] = useState(false);
const [pendingGenerateData, setPendingGenerateData] = useState<any>(null);
const [pendingGenerateData, setPendingGenerateData] = useState<OutlineGenerateRequestData | null>(null);
const [selectedCharacterIndices, setSelectedCharacterIndices] = useState<number[]>([]);
// 组织确认相关状态
@@ -274,7 +310,7 @@ export default function Outline() {
setSSEModalVisible(true);
// 准备请求数据
const requestData: any = {
const requestData: OutlineGenerateRequestData = {
project_id: currentProject.id,
genre: currentProject.genre || '通用',
theme: values.theme || currentProject.theme || '',
@@ -315,10 +351,10 @@ export default function Outline() {
setSSEMessage(msg);
setSSEProgress(progress);
},
onResult: (data: any) => {
onResult: (data: unknown) => {
console.log('生成完成,结果:', data);
},
onCharacterConfirmation: (data: any) => {
onCharacterConfirmation: (data: CharacterConfirmationData) => {
// ✨ 新增:处理角色确认事件
console.log('收到角色确认请求:', data);
// 关闭SSE进度Modal
@@ -332,7 +368,7 @@ export default function Outline() {
setCharacterConfirmData(data);
setCharacterConfirmVisible(true);
},
onOrganizationConfirmation: (data: any) => {
onOrganizationConfirmation: (data: OrganizationConfirmationData) => {
// ✨ 新增:处理组织确认事件
console.log('收到组织确认请求:', data);
// 关闭SSE进度Modal
@@ -396,7 +432,7 @@ export default function Outline() {
defaultModel = settings.llm_model;
}
}
} catch (error) {
} catch {
console.log('获取模型列表失败,将使用默认模型');
}
}
@@ -756,12 +792,13 @@ export default function Outline() {
message.success('大纲创建成功');
await refreshOutlines();
manualCreateForm.resetFields();
} catch (error: any) {
if (error.message === '序号重复') {
} catch (error: unknown) {
const err = error as Error;
if (err.message === '序号重复') {
// 序号重复错误已经显示了Modal,不需要再显示message
throw error;
}
message.error('创建失败:' + (error.message || '未知错误'));
message.error('创建失败:' + (err.message || '未知错误'));
throw error;
}
}
@@ -970,8 +1007,9 @@ export default function Outline() {
const updatedProject = await projectApi.getProject(currentProject.id);
setCurrentProject(updatedProject);
}
} catch (error: any) {
message.error(error.response?.data?.detail || '删除章节失败');
} catch (error: unknown) {
const apiError = error as ApiError;
message.error(apiError.response?.data?.detail || '删除章节失败');
}
};
@@ -1003,12 +1041,11 @@ export default function Outline() {
title: (
<Space style={{ flexWrap: 'wrap' }}>
<CheckCircleOutlined style={{ color: 'var(--color-success)' }} />
<span></span>
<span>{outlineTitle}</span>
</Space>
),
width: isMobile ? '95%' : 900,
centered: true,
okText: '关闭',
style: isMobile ? {
top: 20,
maxWidth: 'calc(100vw - 16px)',
@@ -1016,11 +1053,12 @@ export default function Outline() {
} : undefined,
styles: {
body: {
maxHeight: isMobile ? 'calc(100vh - 150px)' : 'calc(80vh - 110px)',
overflowY: 'auto'
maxHeight: isMobile ? 'calc(100vh - 200px)' : 'calc(80vh - 60px)',
overflowY: 'auto',
overflowX: 'hidden'
}
},
footer: (_: any, { OkBtn }: any) => (
footer: (
<Space wrap style={{ width: '100%', justifyContent: isMobile ? 'center' : 'flex-end' }}>
<Button
danger
@@ -1053,7 +1091,9 @@ export default function Outline() {
>
({data.chapter_count})
</Button>
<OkBtn />
<Button onClick={() => Modal.destroyAll()}>
</Button>
</Space>
),
content: (
@@ -1335,7 +1375,7 @@ export default function Outline() {
// 确认创建章节 - 使用缓存的规划数据,避免重复AI调用
const handleConfirmCreateChapters = async (
outlineId: string,
cachedPlans: any[]
cachedPlans: ChapterPlanItem[]
) => {
try {
setIsExpanding(true);
@@ -1449,7 +1489,7 @@ export default function Outline() {
setSSEMessage(msg);
setSSEProgress(progress);
},
onResult: (data: any) => {
onResult: (data: BatchOutlineExpansionResponse) => {
console.log('批量展开完成,结果:', data);
// 缓存AI生成的规划数据
setCachedBatchExpansionResponse(data);
@@ -1515,7 +1555,7 @@ export default function Outline() {
</div>
<Space direction="vertical" size="small" style={{ width: '100%' }}>
{batchPreviewData.skipped_outlines.map((skipped: any, idx: number) => (
{batchPreviewData.skipped_outlines.map((skipped: SkippedOutlineInfo, idx: number) => (
<div key={idx} style={{ fontSize: 13, color: '#666' }}>
{skipped.outline_title} <Tag color="default" style={{ fontSize: 11 }}>{skipped.reason}</Tag>
</div>
@@ -1581,7 +1621,7 @@ export default function Outline() {
<List
size="small"
dataSource={batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans}
renderItem={(plan: any, idx: number) => (
renderItem={(plan: ChapterPlanItem, idx: number) => (
<List.Item
key={idx}
onClick={() => setSelectedChapterIdx(idx)}
@@ -1625,7 +1665,7 @@ export default function Outline() {
<Card size="small" title="关键事件" bordered={false}>
<Space direction="vertical" size="small" style={{ width: '100%' }}>
{batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].key_events.map((event: string, eventIdx: number) => (
{(batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].key_events as string[]).map((event: string, eventIdx: number) => (
<div key={eventIdx}> {event}</div>
))}
</Space>
@@ -1633,7 +1673,7 @@ export default function Outline() {
<Card size="small" title="涉及角色" bordered={false}>
<Space wrap>
{batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].character_focus.map((char: string, charIdx: number) => (
{(batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].character_focus as string[]).map((char: string, charIdx: number) => (
<Tag key={charIdx} color="purple">{char}</Tag>
))}
</Space>
@@ -1642,7 +1682,7 @@ export default function Outline() {
{batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].scenes && batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].scenes!.length > 0 && (
<Card size="small" title="场景" bordered={false}>
<Space direction="vertical" size="small" style={{ width: '100%' }}>
{batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].scenes!.map((scene: any, sceneIdx: number) => (
{batchPreviewData.expansion_results[selectedOutlineIdx].chapter_plans[selectedChapterIdx].scenes!.map((scene: SceneInfo, sceneIdx: number) => (
<Card key={sceneIdx} size="small" style={{ backgroundColor: '#fafafa' }}>
<div><strong></strong>{scene.location}</div>
<div><strong></strong>{scene.characters.join('、')}</div>
@@ -1700,8 +1740,10 @@ export default function Outline() {
result.chapter_plans
);
totalCreated += response.chapters_created;
} catch (error: any) {
const errorMsg = error.response?.data?.detail || error.message || '未知错误';
} catch (error: unknown) {
const apiError = error as ApiError;
const err = error as Error;
const errorMsg = apiError.response?.data?.detail || err.message || '未知错误';
errors.push(`${result.outline_title}: ${errorMsg}`);
console.error(`创建大纲 ${result.outline_title} 的章节失败:`, error);
}
@@ -1766,7 +1808,7 @@ export default function Outline() {
setSSEMessage(msg);
setSSEProgress(progress);
},
onResult: (data: any) => {
onResult: (data: unknown) => {
console.log('生成完成,结果:', data);
},
onError: (error: string) => {
@@ -1784,7 +1826,7 @@ export default function Outline() {
// 刷新大纲列表
refreshOutlines();
},
onOrganizationConfirmation: (data: any) => {
onOrganizationConfirmation: (data: OrganizationConfirmationData) => {
// 处理可能的后续组织确认
console.log('收到组织确认请求:', data);
setSSEModalVisible(false);
@@ -1836,10 +1878,10 @@ export default function Outline() {
setSSEMessage(msg);
setSSEProgress(progress);
},
onResult: (data: any) => {
onResult: (data: unknown) => {
console.log('生成完成,结果:', data);
},
onOrganizationConfirmation: (data: any) => {
onOrganizationConfirmation: (data: OrganizationConfirmationData) => {
// 处理可能的后续组织确认
console.log('收到组织确认请求:', data);
setSSEModalVisible(false);
@@ -1893,7 +1935,8 @@ export default function Outline() {
// 准备请求数据,添加确认的组织
// ⚠️ 移除 confirmed_characters,避免重复创建角色
const { confirmed_characters, ...baseData } = pendingGenerateData;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { confirmed_characters: _unusedChars, ...baseData } = pendingGenerateData;
const requestData = {
...baseData,
confirmed_organizations: selectedOrganizations
@@ -1908,7 +1951,7 @@ export default function Outline() {
setSSEMessage(msg);
setSSEProgress(progress);
},
onResult: (data: any) => {
onResult: (data: unknown) => {
console.log('生成完成,结果:', data);
},
onError: (error: string) => {
@@ -1955,7 +1998,8 @@ export default function Outline() {
setSSEModalVisible(true);
// 准备请求数据,禁用自动组织引入
const { confirmed_characters, ...baseData } = pendingGenerateData;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { confirmed_characters: _unusedChars, ...baseData } = pendingGenerateData;
const requestData = {
...baseData,
enable_auto_organizations: false // 禁用自动组织引入
@@ -1970,7 +2014,7 @@ export default function Outline() {
setSSEMessage(msg);
setSSEProgress(progress);
},
onResult: (data: any) => {
onResult: (data: unknown) => {
console.log('生成完成,结果:', data);
},
onError: (error: string) => {