feat(engine): merge cloudflare-migration — paradigm D engine, BYOK proxy, story persistence (#95)

Squash-merge the cloudflare-migration branch (7 commits by Kai ki) into
staging with conflict resolution, feature integration, and bug fixes.

Engine:
- Paradigm D: single-stream Writer replacing dual-phase Plan/Beats
- Delete Architect agent; story bible generated via Writer <plan> tag
- Modular prompt architecture (segments/registry/builder)
- StreamRouter for tagged stream splitting (<plan>/<story>/<choices>)

Infrastructure:
- Cloudflare Workers deployment (wrangler.jsonc, OpenNext adapter)
- D1 database schema + Drizzle ORM (scaffolded, not yet active)
- R2 storage helpers (scaffolded, not yet active)
- Story persistence API routes + client-side persistence

BYOK (Bring Your Own Key):
- /api/llm/user-proxy with SSRF-protected LLM proxy (+ requireUser auth)
- CORS-aware fetch in ai-client: auto-detect CORS failure, fallback to
  server proxy transparently via OpenAI SDK custom fetch
- BYO config support added to classify-freeform and vision routes
- SettingsModal CORS privacy notice (keys never logged/stored)

SSE streaming:
- engineClient.ts: fetchSSE helper for progressive scene events
- startSession/requestScene accept optional emit callback
- Fix SSE error event field name (error → message) in scene/start routes

i18n integration:
- Wire buildLanguageDirective into paradigm D's prompt builder
- Update corsNotice i18n keys (zh-CN/en/ja) with CORS proxy privacy text
- Preserve Session.language + LanguageSwitcher from i18n commit

Co-authored-by: Kai ki <155355644+zbf1009@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Zonghao Yuan
2026-06-18 18:05:38 +08:00
committed by GitHub
parent 05bd7e229c
commit 0e4c2ebef4
78 changed files with 7396 additions and 919 deletions
+17 -20
View File
@@ -8,6 +8,7 @@ import type {
FreeformClassifyResponse,
InsertBeatRequest,
InsertBeatResponse,
SceneStreamEvent,
Session,
SceneRequest,
SceneResponse,
@@ -19,7 +20,6 @@ import type {
import { coerceOrientation } from "@infiplot/types";
import { chat } from "@infiplot/ai-client";
import { isStepfun, isValidStepfunVoiceId, provisionVoice } from "@infiplot/tts-client";
import { runArchitect } from "./agents/architect";
import { selectStyle } from "./agents/styleSelector";
import { directInsertBeat, directScene } from "./director";
import { STYLE_MAP } from "@/lib/options";
@@ -51,6 +51,7 @@ function tlog(label: string, t0: number): void {
export async function startSession(
config: EngineConfig,
req: StartRequest,
emit?: (event: SceneStreamEvent) => void,
): Promise<StartResponse> {
const tTotal = Date.now();
@@ -67,38 +68,32 @@ export async function startSession(
language: req.language?.trim() || undefined,
};
// Stage 0 — Architect (+ optional auto style selection, in parallel).
// Both only depend on worldSetting, so they run concurrently.
// Stage 0 — optional auto style selection. The story bible is no longer
// generated by a separate Architect call; the Writer's <plan> produces it
// on the opening scene (paradigm: Writer is the single content brain).
console.log(
`[start] worldSetting (${session.worldSetting.length} chars):\n${session.worldSetting}`,
);
const isAutoStyle = session.styleGuide === "auto";
if (isAutoStyle) {
session.styleGuide = "由 AI 根据剧情自动匹配最佳画风";
}
const tArchitect = Date.now();
const [architectResult, autoStyleGuide] = await Promise.all([
runArchitect(config.text, session),
isAutoStyle
? selectStyle(config.text, session.worldSetting).catch((err) => {
console.warn(`[styleSelector] failed, falling back to 吉卜力:`, err);
return null;
})
: Promise.resolve(null),
]);
session.storyState = architectResult;
if (isAutoStyle) {
const tStyle = Date.now();
const autoStyleGuide = await selectStyle(
config.text,
session.worldSetting,
).catch((err) => {
console.warn(`[styleSelector] failed, falling back to 吉卜力:`, err);
return null;
});
session.styleGuide = autoStyleGuide ?? STYLE_MAP["吉卜力"]!;
tlog("[start] StyleSelector", tStyle);
console.log(`[start] auto-selected style: ${session.styleGuide.slice(0, 60)}`);
}
tlog("[start] Architect" + (isAutoStyle ? " + StyleSelector" : ""), tArchitect);
console.log(
`[start] storyBible: logline="${session.storyState.logline}" | genreTags="${session.storyState.genreTags}" | synopsis="${session.storyState.synopsis}"`,
);
const { scene, sceneImageUrl, characters, storyState } = await directScene(
config,
session,
emit,
);
tlog("[start] TOTAL", tTotal);
@@ -119,12 +114,14 @@ export async function startSession(
export async function requestScene(
config: EngineConfig,
req: SceneRequest,
emit?: (event: SceneStreamEvent) => void,
): Promise<SceneResponse> {
const tTotal = Date.now();
const { scene, sceneImageUrl, characters, storyState } = await directScene(
config,
req.session,
emit,
);
tlog("[scene] TOTAL", tTotal);