第11环裂变重写:对齐上线版 split.js 一次LLM出N套完整笔记包

架构从"扇出N个GenerationTask各跑完整管道"改为"一次LLM调用直接出N套
完整笔记包(N=1~5)",落 FissionNote 表 + 独立展示页。

后端:
- 018迁移:fission_notes 表(文案JSON+score+passed+imagePlan+images+status)
- fission_prompt:FISSION_SYSTEM+三档参考度(low/mid/high)+normalize_tags+品类兜底
- fission_pipeline:一次LLM出N套→各评分(@80合格线)→排序→落库,不达标标
  needs_optimization 非丢弃;apiports 503 回落 codeproxy gpt-5.5 强档兜底
- fission_images:每套串行调现有生图接口(零改动image_gen/storyboard)
- tasks.py:run_fission_pipeline Celery task,删旧扇出注入
- api/v1/fission:进度聚合FissionNote + GET /fission/{id}/notes(剥内部字段)

前端:FissionProgress对齐状态机 + N套独立展示页 + FissionNoteCard

测试:test_fission_engine(19)+test_fission_pipeline(5) 全过;104 全量回归绿

实测task5(fanout=2,mid)端到端跑通:一次出2套→seq0=85过/seq1=79标优化→
生图codeproxy/edits→1024×1536去AI化→task completed→notes端点返完整数据

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
yangqianqian
2026-06-18 11:17:37 +08:00
parent 7f419f4c8b
commit d85dcd401b
18 changed files with 1772 additions and 106 deletions

View File

@@ -0,0 +1,112 @@
'use client';
/**
* /fission/[id]/notes — N 套裂变笔记包独立展示页F2新架构
* 一次 LLM 出的 N 套完整笔记包:标题/正文/标签/封面钩子/imagePlan/生成图/分数。
* 数据源 GET /api/v1/fission/{id}/notesFissionNote 落库结果)。
*/
import { useEffect, useState, useCallback } from 'react';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { api } from '@/lib/api';
import { FissionNoteCard } from '@/components/fission/FissionNoteCard';
export interface FissionImagePlan {
role?: string;
title?: string;
overlayText?: string;
text?: string;
}
export interface FissionNoteData {
title?: string;
content?: string;
tags?: string[];
coverTitle?: string;
dimension?: string;
audience?: string;
scene?: string;
painPoint?: string;
keywords?: string[];
imagePlan?: FissionImagePlan[];
}
export interface FissionImage {
role: string;
seq?: number;
url?: string;
error?: string;
}
export interface FissionNoteItem {
seq: number;
note: FissionNoteData;
images: FissionImage[];
score: number | null;
passed: boolean;
needs_optimization: boolean;
dimension: string | null;
status: string;
}
interface NotesResponse {
fission_id: number;
status: string;
fanout_count: number;
notes: FissionNoteItem[];
}
export default function FissionNotesPage() {
const params = useParams();
const fissionId = Number(params?.id);
const [data, setData] = useState<NotesResponse | null>(null);
const [error, setError] = useState<string | null>(null);
const fetchNotes = useCallback(async () => {
setError(null);
try {
const res = await api.get<NotesResponse>(`/api/v1/fission/${fissionId}/notes`);
setData(res);
} catch {
setError('加载裂变笔记包失败');
}
}, [fissionId]);
useEffect(() => { if (fissionId) fetchNotes(); }, [fissionId, fetchNotes]);
return (
<div className="flex flex-col gap-6 p-6 max-w-4xl">
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-semibold text-text-primary">
#{fissionId} · {data?.fanout_count ?? ''}
</h1>
<p className="text-sm text-text-secondary mt-1">
</p>
</div>
<Link href="/fission" className="text-sm text-text-secondary underline">
</Link>
</div>
{error && (
<div className="rounded-lg bg-red-50 border border-red-200 p-4 text-sm text-red-600">
{error}
<button className="ml-4 underline" onClick={fetchNotes}></button>
</div>
)}
{!data && !error && (
<div className="py-12 text-center text-text-tertiary text-sm"></div>
)}
{data && data.notes.length === 0 && (
<div className="py-12 text-center text-text-tertiary text-sm">
</div>
)}
<div className="flex flex-col gap-6">
{data?.notes.map((item) => (
<FissionNoteCard key={item.seq} item={item} />
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,76 @@
'use client';
/**
* /fission — 独立裂变页第11环倩倩姐2026-06-15拍板"新建独立裂变页"
* 选一个已通过/已归档的爆款源任务 → 设定参考强度+扇出套数 → 一键裂变成 N 套完整笔记包。
* 后端POST /api/v1/fissionGET /api/v1/fission/{id} 查聚合进度。
*/
import { useEffect, useState, useCallback } from 'react';
import { api } from '@/lib/api';
import { TaskListItem, PaginatedResponse } from '@/types';
import { getErrorAction } from '@/types/errors';
import { FissionLauncher } from '@/components/fission/FissionLauncher';
import { FissionProgress } from '@/components/fission/FissionProgress';
// 裂变源 = 已验证的爆款,只从通过/归档里选
const SOURCE_STATUSES = ['approved', 'archived'];
export default function FissionPage() {
const [sources, setSources] = useState<TaskListItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [activeFissionId, setActiveFissionId] = useState<number | null>(null);
const fetchSources = useCallback(async () => {
setLoading(true);
setError(null);
try {
const params = new URLSearchParams({ page: '1', page_size: '50' });
SOURCE_STATUSES.forEach((s) => params.append('status', s));
const res = await api.get<PaginatedResponse<TaskListItem>>(`/api/v1/tasks?${params}`);
setSources(res.items);
} catch (e: unknown) {
const ae = e as { code?: number };
setError(getErrorAction(ae.code ?? 50001).message);
} finally {
setLoading(false);
}
}, []);
useEffect(() => { fetchSources(); }, [fetchSources]);
return (
<div className="flex flex-col gap-6 p-6 max-w-3xl">
<div>
<h1 className="text-xl font-semibold text-text-primary"></h1>
<p className="text-sm text-text-secondary mt-1">
</p>
</div>
{error && (
<div className="rounded-lg bg-red-50 border border-red-200 p-4 text-sm text-red-600">
{error}
<button className="ml-4 underline" onClick={fetchSources}></button>
</div>
)}
{activeFissionId ? (
<FissionProgress
fissionId={activeFissionId}
onReset={() => { setActiveFissionId(null); fetchSources(); }}
/>
) : loading ? (
<div className="py-12 text-center text-text-tertiary text-sm"></div>
) : sources.length === 0 ? (
<div className="py-12 text-center text-text-tertiary text-sm">
</div>
) : (
<FissionLauncher
sources={sources}
onLaunched={(fissionId) => setActiveFissionId(fissionId)}
/>
)}
</div>
);
}