第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:
112
frontend/src/app/fission/[id]/notes/page.tsx
Normal file
112
frontend/src/app/fission/[id]/notes/page.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
'use client';
|
||||
/**
|
||||
* /fission/[id]/notes — N 套裂变笔记包独立展示页(F2,新架构)
|
||||
* 一次 LLM 出的 N 套完整笔记包:标题/正文/标签/封面钩子/imagePlan/生成图/分数。
|
||||
* 数据源 GET /api/v1/fission/{id}/notes(FissionNote 落库结果)。
|
||||
*/
|
||||
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>
|
||||
);
|
||||
}
|
||||
76
frontend/src/app/fission/page.tsx
Normal file
76
frontend/src/app/fission/page.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
'use client';
|
||||
/**
|
||||
* /fission — 独立裂变页(第11环,倩倩姐2026-06-15拍板"新建独立裂变页")
|
||||
* 选一个已通过/已归档的爆款源任务 → 设定参考强度+扇出套数 → 一键裂变成 N 套完整笔记包。
|
||||
* 后端:POST /api/v1/fission,GET /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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user