feature: 新增伏笔管理系统,支持可视化追踪、AI智能关联回收及章节生成时的伏笔提醒

This commit is contained in:
xiamuceer-j
2026-01-19 17:24:37 +08:00
parent 927072d16f
commit 5f25deb289
19 changed files with 4068 additions and 91 deletions
+2
View File
@@ -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 />} /> */}
+12 -12
View File
@@ -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
+7
View File
@@ -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';
+1 -1
View File
@@ -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
+83 -2
View File
@@ -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
),
};
+140
View File
@@ -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[];
}