feature: 新增伏笔管理系统,支持可视化追踪、AI智能关联回收及章节生成时的伏笔提醒
This commit is contained in:
@@ -14,6 +14,7 @@ import Organizations from './pages/Organizations';
|
||||
import Chapters from './pages/Chapters';
|
||||
import ChapterReader from './pages/ChapterReader';
|
||||
import ChapterAnalysis from './pages/ChapterAnalysis';
|
||||
import Foreshadows from './pages/Foreshadows';
|
||||
import WritingStyles from './pages/WritingStyles';
|
||||
import Settings from './pages/Settings';
|
||||
import MCPPlugins from './pages/MCPPlugins';
|
||||
@@ -59,6 +60,7 @@ function App() {
|
||||
<Route path="organizations" element={<Organizations />} />
|
||||
<Route path="chapters" element={<Chapters />} />
|
||||
<Route path="chapter-analysis" element={<ChapterAnalysis />} />
|
||||
<Route path="foreshadows" element={<Foreshadows />} />
|
||||
<Route path="writing-styles" element={<WritingStyles />} />
|
||||
<Route path="sponsor" element={<Sponsor />} />
|
||||
{/* <Route path="polish" element={<Polish />} /> */}
|
||||
|
||||
@@ -1630,7 +1630,7 @@ export default function Chapters() {
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleOpenEditor(item.id)}
|
||||
>
|
||||
编辑内容
|
||||
编辑
|
||||
</Button>,
|
||||
(() => {
|
||||
const task = analysisTasksMap[item.id];
|
||||
@@ -1650,7 +1650,7 @@ export default function Chapters() {
|
||||
''
|
||||
}
|
||||
>
|
||||
{isAnalyzing ? '分析中' : '查看分析'}
|
||||
{isAnalyzing ? '分析中' : '分析'}
|
||||
</Button>
|
||||
);
|
||||
})(),
|
||||
@@ -1659,7 +1659,7 @@ export default function Chapters() {
|
||||
icon={<SettingOutlined />}
|
||||
onClick={() => handleOpenModal(item.id)}
|
||||
>
|
||||
修改信息
|
||||
修改
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
@@ -1716,7 +1716,7 @@ export default function Chapters() {
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleOpenEditor(item.id)}
|
||||
size="small"
|
||||
title="编辑内容"
|
||||
title="编辑"
|
||||
/>
|
||||
{(() => {
|
||||
const task = analysisTasksMap[item.id];
|
||||
@@ -1734,7 +1734,7 @@ export default function Chapters() {
|
||||
title={
|
||||
!hasContent ? '请先生成章节内容' :
|
||||
isAnalyzing ? '分析中' :
|
||||
'查看分析'
|
||||
'分析'
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -1744,7 +1744,7 @@ export default function Chapters() {
|
||||
icon={<SettingOutlined />}
|
||||
onClick={() => handleOpenModal(item.id)}
|
||||
size="small"
|
||||
title="修改信息"
|
||||
title="修改"
|
||||
/>
|
||||
</Space>
|
||||
)}
|
||||
@@ -1815,7 +1815,7 @@ export default function Chapters() {
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleOpenEditor(item.id)}
|
||||
>
|
||||
编辑内容
|
||||
编辑
|
||||
</Button>,
|
||||
(() => {
|
||||
const task = analysisTasksMap[item.id];
|
||||
@@ -1835,7 +1835,7 @@ export default function Chapters() {
|
||||
''
|
||||
}
|
||||
>
|
||||
{isAnalyzing ? '分析中' : '查看分析'}
|
||||
{isAnalyzing ? '分析中' : '分析'}
|
||||
</Button>
|
||||
);
|
||||
})(),
|
||||
@@ -1844,7 +1844,7 @@ export default function Chapters() {
|
||||
icon={<SettingOutlined />}
|
||||
onClick={() => handleOpenModal(item.id)}
|
||||
>
|
||||
修改信息
|
||||
修改
|
||||
</Button>,
|
||||
// 只在 one-to-many 模式下显示删除按钮
|
||||
...(currentProject.outline_mode === 'one-to-many' ? [
|
||||
@@ -1940,7 +1940,7 @@ export default function Chapters() {
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleOpenEditor(item.id)}
|
||||
size="small"
|
||||
title="编辑内容"
|
||||
title="编辑"
|
||||
/>
|
||||
{(() => {
|
||||
const task = analysisTasksMap[item.id];
|
||||
@@ -1958,7 +1958,7 @@ export default function Chapters() {
|
||||
title={
|
||||
!hasContent ? '请先生成章节内容' :
|
||||
isAnalyzing ? '分析中' :
|
||||
'查看分析'
|
||||
'分析'
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -1968,7 +1968,7 @@ export default function Chapters() {
|
||||
icon={<SettingOutlined />}
|
||||
onClick={() => handleOpenModal(item.id)}
|
||||
size="small"
|
||||
title="修改信息"
|
||||
title="修改"
|
||||
/>
|
||||
{/* 只在 one-to-many 模式下显示删除按钮 */}
|
||||
{currentProject.outline_mode === 'one-to-many' && (
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@ import {
|
||||
FundOutlined,
|
||||
HeartOutlined,
|
||||
TrophyOutlined,
|
||||
BulbOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useStore } from '../store';
|
||||
import { useCharacterSync, useOutlineSync, useChapterSync } from '../store/hooks';
|
||||
@@ -140,6 +141,11 @@ export default function ProjectDetail() {
|
||||
icon: <FundOutlined />,
|
||||
label: <Link to={`/project/${projectId}/chapter-analysis`}>剧情分析</Link>,
|
||||
},
|
||||
{
|
||||
key: 'foreshadows',
|
||||
icon: <BulbOutlined />,
|
||||
label: <Link to={`/project/${projectId}/foreshadows`}>伏笔管理</Link>,
|
||||
},
|
||||
{
|
||||
key: 'writing-styles',
|
||||
icon: <EditOutlined />,
|
||||
@@ -162,6 +168,7 @@ export default function ProjectDetail() {
|
||||
if (path.includes('/outline')) return 'outline';
|
||||
if (path.includes('/characters')) return 'characters';
|
||||
if (path.includes('/chapter-analysis')) return 'chapter-analysis';
|
||||
if (path.includes('/foreshadows')) return 'foreshadows';
|
||||
if (path.includes('/chapters')) return 'chapters';
|
||||
if (path.includes('/writing-styles')) return 'writing-styles';
|
||||
if (path.includes('/sponsor')) return 'sponsor';
|
||||
|
||||
@@ -122,7 +122,7 @@ export default function ProjectWizardNew() {
|
||||
narrative_perspective: '第三人称',
|
||||
character_count: 5,
|
||||
target_words: 100000,
|
||||
outline_mode: 'one-to-many', // 默认为细化模式
|
||||
outline_mode: 'one-to-one', // 默认为传统模式(1-1)
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
|
||||
@@ -470,7 +470,7 @@ export const outlineApi = {
|
||||
|
||||
export const characterApi = {
|
||||
getCharacters: (projectId: string) =>
|
||||
api.get<unknown, Character[]>(`/characters/project/${projectId}`),
|
||||
api.get<unknown, { total: number; items: Character[] }>(`/characters/project/${projectId}`).then(res => res.items),
|
||||
|
||||
getCharacter: (id: string) => api.get<unknown, Character>(`/characters/${id}`),
|
||||
|
||||
@@ -582,7 +582,7 @@ export const characterApi = {
|
||||
|
||||
export const chapterApi = {
|
||||
getChapters: (projectId: string) =>
|
||||
api.get<unknown, Chapter[]>(`/chapters/project/${projectId}`),
|
||||
api.get<unknown, { total: number; items: Chapter[] }>(`/chapters/project/${projectId}`).then(res => res.items),
|
||||
|
||||
getChapter: (id: string) => api.get<unknown, Chapter>(`/chapters/${id}`),
|
||||
|
||||
@@ -920,4 +920,85 @@ export const adminApi = {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}>(`/admin/users/${userId}`),
|
||||
};
|
||||
|
||||
// 伏笔管理API
|
||||
export const foreshadowApi = {
|
||||
// 获取项目伏笔列表
|
||||
getProjectForeshadows: (projectId: string, params?: {
|
||||
status?: string;
|
||||
category?: string;
|
||||
source_type?: string;
|
||||
is_long_term?: boolean;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}) =>
|
||||
api.get<unknown, import('../types').ForeshadowListResponse>(
|
||||
`/foreshadows/projects/${projectId}`,
|
||||
{ params }
|
||||
),
|
||||
|
||||
// 获取伏笔统计
|
||||
getForeshadowStats: (projectId: string, currentChapter?: number) =>
|
||||
api.get<unknown, import('../types').ForeshadowStats>(
|
||||
`/foreshadows/projects/${projectId}/stats`,
|
||||
{ params: { current_chapter: currentChapter } }
|
||||
),
|
||||
|
||||
// 获取章节伏笔上下文
|
||||
getChapterContext: (projectId: string, chapterNumber: number, params?: {
|
||||
include_pending?: boolean;
|
||||
include_overdue?: boolean;
|
||||
lookahead?: number;
|
||||
}) =>
|
||||
api.get<unknown, import('../types').ForeshadowContextResponse>(
|
||||
`/foreshadows/projects/${projectId}/context/${chapterNumber}`,
|
||||
{ params }
|
||||
),
|
||||
|
||||
// 获取待回收伏笔
|
||||
getPendingResolveForeshadows: (projectId: string, currentChapter: number, lookahead?: number) =>
|
||||
api.get<unknown, { total: number; items: import('../types').Foreshadow[] }>(
|
||||
`/foreshadows/projects/${projectId}/pending-resolve`,
|
||||
{ params: { current_chapter: currentChapter, lookahead } }
|
||||
),
|
||||
|
||||
// 获取单个伏笔
|
||||
getForeshadow: (foreshadowId: string) =>
|
||||
api.get<unknown, import('../types').Foreshadow>(`/foreshadows/${foreshadowId}`),
|
||||
|
||||
// 创建伏笔
|
||||
createForeshadow: (data: import('../types').ForeshadowCreate) =>
|
||||
api.post<unknown, import('../types').Foreshadow>('/foreshadows', data),
|
||||
|
||||
// 更新伏笔
|
||||
updateForeshadow: (foreshadowId: string, data: import('../types').ForeshadowUpdate) =>
|
||||
api.put<unknown, import('../types').Foreshadow>(`/foreshadows/${foreshadowId}`, data),
|
||||
|
||||
// 删除伏笔
|
||||
deleteForeshadow: (foreshadowId: string) =>
|
||||
api.delete<unknown, { message: string; id: string }>(`/foreshadows/${foreshadowId}`),
|
||||
|
||||
// 标记伏笔为已埋入
|
||||
plantForeshadow: (foreshadowId: string, data: import('../types').PlantForeshadowRequest) =>
|
||||
api.post<unknown, import('../types').Foreshadow>(`/foreshadows/${foreshadowId}/plant`, data),
|
||||
|
||||
// 标记伏笔为已回收
|
||||
resolveForeshadow: (foreshadowId: string, data: import('../types').ResolveForeshadowRequest) =>
|
||||
api.post<unknown, import('../types').Foreshadow>(`/foreshadows/${foreshadowId}/resolve`, data),
|
||||
|
||||
// 标记伏笔为已废弃
|
||||
abandonForeshadow: (foreshadowId: string, reason?: string) =>
|
||||
api.post<unknown, import('../types').Foreshadow>(
|
||||
`/foreshadows/${foreshadowId}/abandon`,
|
||||
null,
|
||||
{ params: { reason } }
|
||||
),
|
||||
|
||||
// 从分析结果同步伏笔
|
||||
syncFromAnalysis: (projectId: string, data: import('../types').SyncFromAnalysisRequest) =>
|
||||
api.post<unknown, import('../types').SyncFromAnalysisResponse>(
|
||||
`/foreshadows/projects/${projectId}/sync-from-analysis`,
|
||||
data
|
||||
),
|
||||
};
|
||||
@@ -740,4 +740,144 @@ export interface MCPToolCallResponse {
|
||||
success: boolean;
|
||||
result?: unknown;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// 伏笔管理类型定义
|
||||
export type ForeshadowStatus = 'pending' | 'planted' | 'resolved' | 'partially_resolved' | 'abandoned';
|
||||
export type ForeshadowSourceType = 'analysis' | 'manual';
|
||||
export type ForeshadowCategory = 'identity' | 'mystery' | 'item' | 'relationship' | 'event' | 'ability' | 'prophecy';
|
||||
|
||||
export interface Foreshadow {
|
||||
id: string;
|
||||
project_id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
hint_text?: string;
|
||||
resolution_text?: string;
|
||||
source_type?: ForeshadowSourceType;
|
||||
source_memory_id?: string;
|
||||
source_analysis_id?: string;
|
||||
plant_chapter_id?: string;
|
||||
plant_chapter_number?: number;
|
||||
target_resolve_chapter_id?: string;
|
||||
target_resolve_chapter_number?: number;
|
||||
actual_resolve_chapter_id?: string;
|
||||
actual_resolve_chapter_number?: number;
|
||||
status: ForeshadowStatus;
|
||||
is_long_term: boolean;
|
||||
importance: number;
|
||||
strength: number;
|
||||
subtlety: number;
|
||||
urgency: number;
|
||||
related_characters?: string[];
|
||||
related_foreshadow_ids?: string[];
|
||||
tags?: string[];
|
||||
category?: ForeshadowCategory;
|
||||
notes?: string;
|
||||
resolution_notes?: string;
|
||||
auto_remind: boolean;
|
||||
remind_before_chapters: number;
|
||||
include_in_context: boolean;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
planted_at?: string;
|
||||
resolved_at?: string;
|
||||
}
|
||||
|
||||
export interface ForeshadowCreate {
|
||||
project_id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
hint_text?: string;
|
||||
resolution_text?: string;
|
||||
plant_chapter_number?: number;
|
||||
target_resolve_chapter_number?: number;
|
||||
is_long_term?: boolean;
|
||||
importance?: number;
|
||||
strength?: number;
|
||||
subtlety?: number;
|
||||
related_characters?: string[];
|
||||
tags?: string[];
|
||||
category?: ForeshadowCategory;
|
||||
notes?: string;
|
||||
resolution_notes?: string;
|
||||
auto_remind?: boolean;
|
||||
remind_before_chapters?: number;
|
||||
include_in_context?: boolean;
|
||||
}
|
||||
|
||||
export interface ForeshadowUpdate {
|
||||
title?: string;
|
||||
content?: string;
|
||||
hint_text?: string;
|
||||
resolution_text?: string;
|
||||
plant_chapter_number?: number;
|
||||
target_resolve_chapter_number?: number;
|
||||
status?: ForeshadowStatus;
|
||||
is_long_term?: boolean;
|
||||
importance?: number;
|
||||
strength?: number;
|
||||
subtlety?: number;
|
||||
urgency?: number;
|
||||
related_characters?: string[];
|
||||
related_foreshadow_ids?: string[];
|
||||
tags?: string[];
|
||||
category?: ForeshadowCategory;
|
||||
notes?: string;
|
||||
resolution_notes?: string;
|
||||
auto_remind?: boolean;
|
||||
remind_before_chapters?: number;
|
||||
include_in_context?: boolean;
|
||||
}
|
||||
|
||||
export interface ForeshadowStats {
|
||||
total: number;
|
||||
pending: number;
|
||||
planted: number;
|
||||
resolved: number;
|
||||
partially_resolved: number;
|
||||
abandoned: number;
|
||||
long_term_count: number;
|
||||
overdue_count: number;
|
||||
}
|
||||
|
||||
export interface ForeshadowListResponse {
|
||||
total: number;
|
||||
items: Foreshadow[];
|
||||
stats?: ForeshadowStats;
|
||||
}
|
||||
|
||||
export interface PlantForeshadowRequest {
|
||||
chapter_id: string;
|
||||
chapter_number: number;
|
||||
hint_text?: string;
|
||||
}
|
||||
|
||||
export interface ResolveForeshadowRequest {
|
||||
chapter_id: string;
|
||||
chapter_number: number;
|
||||
resolution_text?: string;
|
||||
is_partial?: boolean;
|
||||
}
|
||||
|
||||
export interface SyncFromAnalysisRequest {
|
||||
chapter_ids?: string[];
|
||||
overwrite_existing?: boolean;
|
||||
auto_set_planted?: boolean;
|
||||
}
|
||||
|
||||
export interface SyncFromAnalysisResponse {
|
||||
synced_count: number;
|
||||
skipped_count: number;
|
||||
new_foreshadows: Foreshadow[];
|
||||
skipped_reasons: Array<{ source_memory_id: string; reason: string }>;
|
||||
}
|
||||
|
||||
export interface ForeshadowContextResponse {
|
||||
chapter_number: number;
|
||||
context_text: string;
|
||||
pending_plant: Foreshadow[];
|
||||
pending_resolve: Foreshadow[];
|
||||
overdue: Foreshadow[];
|
||||
recently_planted: Foreshadow[];
|
||||
}
|
||||
Reference in New Issue
Block a user