auto-save 2026-05-19 00:01 (~8)
This commit is contained in:
@@ -143,6 +143,19 @@
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 2 项未提交变更 · 最近提交:auto-save 2026-05-18 23:50 (~2, -1)",
|
||||
"files_changed": 2
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-18T23:55:42+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-18 23:55 (+5, ~9)",
|
||||
"hash": "4eda85e",
|
||||
"files_changed": 16
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-18T15:56:50Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Claude 会话活跃 · 最近命令:claude · 分支 master · 8 项未提交变更 · 最近提交:auto-save 2026-05-18 23:55 (+5, ~9)",
|
||||
"files_changed": 8
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
{ "type": "app", "url": "http://localhost:4560", "label": "本地 dev" }
|
||||
],
|
||||
"credentials": [
|
||||
{ "name": "POE_API_KEY", "env": "POE_API_KEY", "note": "调 nano-banana-pro 生图,没填则 mock" }
|
||||
{ "name": "OPENAI_API_KEY", "env": "OPENAI_API_KEY", "note": "GPT 文本/结构化/图片生成;没填则图片 mock" },
|
||||
{ "name": "SEEDANCE_API_KEY", "env": "SEEDANCE_API_KEY", "note": "Seedance 视频生成;没填则视频接口不可用" }
|
||||
],
|
||||
"ports": [
|
||||
{
|
||||
@@ -22,7 +23,7 @@
|
||||
"auto": true
|
||||
},
|
||||
"stack": [
|
||||
"Next.js + nano-banana-pro"
|
||||
"Next.js + GPT + Seedance"
|
||||
],
|
||||
"ownership": "personal"
|
||||
}
|
||||
|
||||
24
RULES.md
24
RULES.md
@@ -21,13 +21,20 @@
|
||||
- 数据持久化在 `data/`(gitignored),不入库
|
||||
|
||||
## 环境变量
|
||||
- `POE_API_KEY` — Poe API Key,调 `nano-banana-pro` 生图;不填则回退 mock(SVG 占位图)
|
||||
- `OPENAI_API_KEY` — GPT API Key;文本/结构化/图片生成统一走 GPT 最高规格配置
|
||||
- `GPT_TEXT_MODEL` — 默认 `gpt-5.5`,用于角色设定等结构化输出
|
||||
- `GPT_IMAGE_MODEL` — 默认 `gpt-image-1`,用于意向图和三类素材包图片生成
|
||||
- `GPT_API_BASE` — 默认 `https://api.openai.com/v1`
|
||||
- `SEEDANCE_API_KEY` — Seedance 视频生成 Key;未配置时视频接口返回 503
|
||||
- `SEEDANCE_MODEL` — 默认 `seedance-1-0-pro`
|
||||
- `SEEDANCE_API_BASE` — 默认 `https://ark.cn-beijing.volces.com/api/v3`
|
||||
- 配置位置:`.env.local`(gitignored),参考 `.env.local.example`
|
||||
- credentials.md 里旧 Poe Key 已"离职清除",**需要重新申请**或临时跑 mock
|
||||
- 图片生成未配置 GPT Key 时回退 mock(SVG 占位图),视频生成不 mock,必须配置 Seedance Key
|
||||
|
||||
## 规则
|
||||
- 全项目规则真源:`/Users/kangwan/Projects/code/20260317-rules-dashboard/RULES.md`
|
||||
- 生图模型固定 `nano-banana-pro`(feedback_image-gen-model)
|
||||
- 文本/结构化/图片生成统一使用 GPT 最高规格配置
|
||||
- 视频生成固定使用 Seedance
|
||||
- 不允许编造不存在的部署域名、账号、密码
|
||||
|
||||
## 注意事项
|
||||
@@ -35,14 +42,17 @@
|
||||
- `refs/` 下放参考资料 PDF/DOCX/图(已 gitignored,但 `refs/README` 之类可放进库)
|
||||
- mock 模式仅用于跑通流程,生图质量为零(只是 SVG 笑脸占位)
|
||||
|
||||
## 工作流(MVP Step1+2)
|
||||
## 工作流
|
||||
1. 输入 prompt + 可选参考图(最多 4 张)+ 风格 + 数量(4/8/12)
|
||||
2. 点 🪄 批量生成 / `⌘/Ctrl+Enter`
|
||||
3. 九宫格快筛:数字键 `1-9` 选中,`Shift+1-9` 打叉
|
||||
4. 选中的图自动复制到 `data/selected/`
|
||||
5. 侧栏保留历史会话,点击切换
|
||||
5. 锁定角色设定 `CharacterSpec`
|
||||
6. 一键生成完整三包:专利包、生产打样包、宣发包
|
||||
7. Seedance 生成视频任务:旋转展示、开箱、触感细节、角色故事
|
||||
8. 侧栏保留历史会话,点击切换
|
||||
|
||||
## 后续路线
|
||||
- Step3:多视图扩展(正/侧/背/45°/俯视)—— 待 Poe Key 接入后做
|
||||
- Step4:尺寸标注 / 三视图蓝图样式 —— 同上
|
||||
- 导出专利包:PNG高清 + PDF合订
|
||||
- ZIP/PDF 打包下载
|
||||
- Seedance 任务轮询 UI
|
||||
|
||||
@@ -42,43 +42,3 @@ export async function POST(req: Request) {
|
||||
return NextResponse.json({ error: String(error) }, { status: 500 });
|
||||
}
|
||||
}
|
||||
import { NextResponse } from 'next/server';
|
||||
import { buildCharacterSpec } from '@/lib/packGenerator';
|
||||
import { detectProvider } from '@/lib/providers';
|
||||
import { loadSession, saveSession } from '@/lib/storage';
|
||||
import type { LockCharacterRequest, LockCharacterResponse } from '@/lib/types';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { sessionId, imageId, force = false } = (await req.json()) as LockCharacterRequest;
|
||||
if (!sessionId || !imageId) {
|
||||
return NextResponse.json({ error: 'sessionId and imageId required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const session = await loadSession(sessionId);
|
||||
if (!session) return NextResponse.json({ error: 'session not found' }, { status: 404 });
|
||||
|
||||
const sourceImage = session.images.find(image => image.id === imageId);
|
||||
if (!sourceImage) return NextResponse.json({ error: 'image not found' }, { status: 404 });
|
||||
|
||||
if (!force && session.characterSpec?.sourceImageId === imageId) {
|
||||
return NextResponse.json({
|
||||
characterSpec: session.characterSpec,
|
||||
provider: detectProvider(),
|
||||
} satisfies LockCharacterResponse);
|
||||
}
|
||||
|
||||
try {
|
||||
const characterSpec = await buildCharacterSpec(session, sourceImage);
|
||||
session.characterSpec = characterSpec;
|
||||
await saveSession(session);
|
||||
return NextResponse.json({
|
||||
characterSpec,
|
||||
provider: detectProvider(),
|
||||
} satisfies LockCharacterResponse);
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: String(error) }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,46 +8,6 @@ export const dynamic = 'force-dynamic';
|
||||
|
||||
const PACK_KINDS: PackKind[] = ['patent', 'production', 'marketing'];
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const body = (await req.json()) as GeneratePackRequest;
|
||||
const { sessionId, imageId, kind } = body;
|
||||
|
||||
if (!sessionId || !imageId || !PACK_KINDS.includes(kind)) {
|
||||
return NextResponse.json({ error: 'sessionId, imageId and valid kind required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const session = await loadSession(sessionId);
|
||||
if (!session) return NextResponse.json({ error: 'session not found' }, { status: 404 });
|
||||
|
||||
const sourceImage = session.images.find(image => image.id === imageId);
|
||||
if (!sourceImage) return NextResponse.json({ error: 'image not found' }, { status: 404 });
|
||||
|
||||
try {
|
||||
const { pack, manifest, provider } = await generateAssetPack({ session, sourceImage, kind });
|
||||
const packs = (session.packs ?? []).filter(item => !(item.kind === kind && item.sourceImageId === imageId));
|
||||
const exports = (session.exports ?? []).filter(item => !(item.packKind === kind && item.source.sourceImageId === imageId));
|
||||
|
||||
session.characterSpec = pack.characterSpec;
|
||||
session.packs = [...packs, pack];
|
||||
session.exports = [...exports, manifest];
|
||||
await saveSession(session);
|
||||
|
||||
const response: GeneratePackResponse = { pack, manifest, provider };
|
||||
return NextResponse.json(response);
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: String(error) }, { status: 500 });
|
||||
}
|
||||
}
|
||||
import { NextResponse } from 'next/server';
|
||||
import { generateAssetPack } from '@/lib/packGenerator';
|
||||
import { loadSession, saveSession } from '@/lib/storage';
|
||||
import type { GeneratePackRequest, GeneratePackResponse, PackKind } from '@/lib/types';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
const PACK_KINDS: PackKind[] = ['patent', 'production', 'marketing'];
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { sessionId, imageId, kind } = (await req.json()) as GeneratePackRequest;
|
||||
if (!sessionId || !imageId || !PACK_KINDS.includes(kind)) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
PACK_LABELS,
|
||||
PACK_ORDER,
|
||||
PACK_TEMPLATES,
|
||||
TEMPLATE_FREEZE_VERSION,
|
||||
VIDEO_TEMPLATES,
|
||||
} from '@/lib/templates';
|
||||
|
||||
@@ -13,6 +14,7 @@ export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
templateFreezeVersion: TEMPLATE_FREEZE_VERSION,
|
||||
filenameSchema: FILENAME_SCHEMA,
|
||||
characterSpecFields: CHARACTER_SPEC_FIELDS,
|
||||
packs: PACK_ORDER.map(kind => ({
|
||||
|
||||
@@ -107,6 +107,7 @@ export type ExportManifest = {
|
||||
version: string;
|
||||
createdAt: number;
|
||||
filenameSchema: string;
|
||||
templateFreezeVersion: string;
|
||||
source: {
|
||||
prompt: string;
|
||||
sourceImageId: string;
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user