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:
+17
-20
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user