Browse Source

处理冲突

TEST-s
TianYun 2 months ago
parent
commit
50035db967
  1. 10
      src/app/api/_popiserverModels.ts
  2. 98
      src/app/api/chat/route.ts
  3. 176
      src/app/api/popi/asset/create/route.ts
  4. 137
      src/app/api/popi/media/upload/route.ts
  5. 8
      src/app/api/quickstart/propose/route.ts
  6. 8
      src/app/api/quickstart/route.ts
  7. 2
      src/components/ProjectSetupModal.tsx
  8. 5
      src/components/composer/GenerationComposer.tsx
  9. 2
      src/lib/llmModels.ts
  10. 89
      src/lib/quickstart/modelClient.ts
  11. 144
      src/store/execution/simpleNodeExecutors.ts
  12. 4
      src/store/utils/buildApiHeaders.ts
  13. 2
      src/store/utils/nodeDefaults.ts

10
src/app/api/_popiserverModels.ts

@ -107,11 +107,17 @@ const SUBTYPE_TO_CAPABILITIES: Record<number, ModelCapability[]> = {
};
export function getPopiserverBaseUrl(request: NextRequest): string {
const requestOrigin =
"nextUrl" in request && request.nextUrl
? request.nextUrl.origin
: "url" in request && request.url
? new URL(request.url).origin
: "";
const baseUrl =
process.env.POPISERVER_BASE_URL ||
process.env.POPIART_API_BASE_URL ||
process.env.POPISERVER_BASE_URL ||
process.env.NEXT_PUBLIC_APP_URL ||
request.nextUrl.origin;
requestOrigin;
return baseUrl.replace(/\/+$/, "");
}

98
src/app/api/chat/route.ts

@ -6,14 +6,88 @@ import { extractSubgraph } from '@/lib/chat/subgraphExtractor';
import { WorkflowNode } from '@/types';
import { WorkflowEdge } from '@/types/workflow';
import { normalizeNewApiWGBaseUrl } from '@/app/api/generate/providers/newapiwg';
import { DEFAULT_NEWAPIWG_LLM_MODEL_ID } from '@/lib/llmModels';
import { DEFAULT_NEWAPIWG_LLM_MODEL_ID, DEFAULT_POPISERVER_LLM_MODEL_ID } from '@/lib/llmModels';
import { buildUserForwardHeaders, requireLogin } from '@/app/api/_auth';
import { ApiError, apiErrorResponse } from '@/app/api/_errors';
import { resolveUserGatewayApiKey } from '@/app/api/_gatewayApiKey';
import { logGatewayRequest, logGatewayResponse } from '@/app/api/_gatewayLogging';
import { isPopiProviderMode } from '@/lib/providerMode';
export const maxDuration = 60; // 1 minute timeout
type PopiserverChatResponse = {
status?: string;
message?: string;
data?: {
choices?: Array<{
message?: {
content?: string;
};
}>;
};
};
function getPopiserverBaseUrl(request: Request): string {
const baseUrl =
process.env.POPIART_API_BASE_URL ||
process.env.NEXT_PUBLIC_APP_URL ||
new URL(request.url).origin;
return baseUrl.replace(/\/+$/, "");
}
function buildPopiserverForwardHeaders(request: Request, token: string): HeadersInit {
const headers: Record<string, string> = {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
token,
};
const cookie = request.headers.get("cookie");
if (cookie) headers.Cookie = cookie;
return headers;
}
async function callPopiserverChat(
request: Request,
token: string,
prompt: string
): Promise<string> {
const url = `${getPopiserverBaseUrl(request)}/api_client/anime/task/llmChat`;
const init: RequestInit = {
method: "POST",
headers: buildPopiserverForwardHeaders(request, token),
body: JSON.stringify({
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: prompt },
],
model: DEFAULT_POPISERVER_LLM_MODEL_ID,
stream: false,
aiModelId: 31,
}),
};
const context = {
model: DEFAULT_POPISERVER_LLM_MODEL_ID,
provider: "popiserver",
};
const startedAt = Date.now();
logGatewayRequest("Calling chat PopiServer API", url, init, context);
const response = await fetch(url, init);
await logGatewayResponse("Chat PopiServer API response", url, init, response, context, startedAt);
const body = (await response.json().catch(() => null)) as PopiserverChatResponse | null;
if (!response.ok) {
throw new Error(body?.message || `PopiServer chat API error: ${response.status}`);
}
if (!body || body.status !== "0000") {
throw new Error(body?.message || "PopiServer chat API returned an error");
}
const text = body.data?.choices?.[0]?.message?.content;
if (!text) throw new Error("No text in PopiServer chat response");
return text;
}
export async function POST(request: Request) {
try {
const login = requireLogin(request);
@ -23,11 +97,6 @@ export async function POST(request: Request) {
selectedNodeIds?: string[];
};
const apiKey = await resolveUserGatewayApiKey(request);
const baseUrl = normalizeNewApiWGBaseUrl(
request.headers.get('X-NewApiWG-Base-URL') || process.env.NEWAPIWG_BASE_URL
);
// Extract subgraph if nodes are selected, otherwise use full workflow
const subgraph = extractSubgraph(
workflowState?.nodes || [],
@ -44,6 +113,23 @@ export async function POST(request: Request) {
// Build context-aware system prompt with optional rest summary
const systemPrompt = buildEditSystemPrompt(context, subgraph.restSummary);
if (isPopiProviderMode()) {
const lastMessage = messages[messages.length - 1] as { parts?: Array<{ type?: string; text?: string }>; content?: string } | undefined;
const userPrompt =
lastMessage?.parts?.map((part) => part.text).filter(Boolean).join("\n") ||
lastMessage?.content ||
"";
const text = await callPopiserverChat(request, login.token, `${systemPrompt}\n\n${userPrompt}`);
return new Response(text, {
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
}
const apiKey = await resolveUserGatewayApiKey(request);
const baseUrl = normalizeNewApiWGBaseUrl(
request.headers.get('X-NewApiWG-Base-URL') || process.env.NEWAPIWG_BASE_URL
);
// Extract node IDs for tool validation
const nodeIds = (workflowState?.nodes || []).map(n => n.id);

176
src/app/api/popi/asset/create/route.ts

@ -0,0 +1,176 @@
import { NextRequest, NextResponse } from "next/server";
import { requireLogin } from "@/app/api/_auth";
import { ApiError } from "@/app/api/_errors";
const POPI_MEDIA_UPLOAD_BASE64_PATH = "/api_client/media/uploadFileBase64";
const POPI_ASSET_CREATE_PATH = "/api_client/anime/asset/create";
type PopiserverResponse<T> = {
status?: string;
message?: string;
data?: T;
};
type PopiMedia = {
path?: string;
url?: string;
originPath?: string;
originUrl?: string;
};
type PopiAsset = {
id?: number;
type?: number;
images?: string[];
video?: string;
};
function getPopiBaseUrl(request: NextRequest): string {
const baseUrl =
process.env.POPIART_API_BASE_URL ||
process.env.NEXT_PUBLIC_APP_URL ||
request.nextUrl.origin;
return baseUrl.replace(/\/+$/, "");
}
function parseDataUrl(value: string): { mimeType: string; base64: string } | undefined {
const match = value.match(/^data:([^;,]+)?(?:;[^,]*)?;base64,(.*)$/);
if (!match) return undefined;
return {
mimeType: match[1] || "application/octet-stream",
base64: match[2],
};
}
function extensionForMimeType(mimeType: string): string {
const normalized = mimeType.toLowerCase();
if (normalized === "image/jpeg") return "jpg";
if (normalized === "image/png") return "png";
if (normalized === "image/webp") return "webp";
if (normalized === "image/gif") return "gif";
if (normalized === "video/mp4") return "mp4";
if (normalized === "video/webm") return "webm";
return normalized.split("/")[1]?.replace(/[^a-z0-9]/g, "") || "bin";
}
function mediaResourceUrl(media: PopiMedia): string | null {
return media.url || media.originUrl || media.path || media.originPath || null;
}
function isPopiserverMediaUrl(value: string): boolean {
if (value.startsWith("/media/") || value.startsWith("/media_private/")) return true;
try {
const url = new URL(value);
return url.pathname.startsWith("/media/") || url.pathname.startsWith("/media_private/");
} catch {
return false;
}
}
async function postPopiJson<T>(
request: NextRequest,
token: string,
path: string,
body: Record<string, unknown>
): Promise<T> {
const upstream = await fetch(getPopiBaseUrl(request) + path, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
token,
},
body: JSON.stringify(body),
cache: "no-store",
});
const payload = (await upstream.json().catch(() => null)) as PopiserverResponse<T> | null;
if (!upstream.ok) {
throw ApiError.provider(payload?.message || "Popiserver API request failed.", upstream.status);
}
if (!payload || payload.status !== "0000") {
throw ApiError.provider(payload?.message || "Popiserver API returned an error.", 502);
}
return payload.data as T;
}
async function ensurePopiserverMedia(
request: NextRequest,
token: string,
value: string,
kind: "image" | "video",
fileName?: string
): Promise<string> {
if (isPopiserverMediaUrl(value)) return value;
const dataUrl = parseDataUrl(value);
if (!dataUrl) {
throw ApiError.badRequest("Only popiserver media URLs or base64 data URLs can be saved as assets.");
}
const extension = extensionForMimeType(dataUrl.mimeType);
const media = await postPopiJson<PopiMedia>(request, token, POPI_MEDIA_UPLOAD_BASE64_PATH, {
data: dataUrl.base64,
fileName: fileName || `canvas-${kind}-${Date.now()}.${extension}`,
});
const url = mediaResourceUrl(media);
if (!url) {
throw ApiError.provider("Popiserver media upload response is missing resource URL.", 502);
}
return url;
}
export async function POST(request: NextRequest) {
try {
const login = requireLogin(request);
const body = (await request.json().catch(() => null)) as {
type?: unknown;
media?: unknown;
title?: unknown;
description?: unknown;
fileName?: unknown;
} | null;
const type = body?.type === "video" ? "video" : body?.type === "image" ? "image" : null;
const media = typeof body?.media === "string" ? body.media : "";
if (!type || !media) {
return NextResponse.json(
{ success: false, error: "type=image|video and media are required." },
{ status: 400 }
);
}
const resourceUrl = await ensurePopiserverMedia(
request,
login.token,
media,
type,
typeof body?.fileName === "string" ? body.fileName : undefined
);
const title = typeof body?.title === "string" ? body.title : "";
const description = typeof body?.description === "string" ? body.description : "";
const asset = await postPopiJson<PopiAsset>(request, login.token, POPI_ASSET_CREATE_PATH, {
type: type === "video" ? 2 : 1,
title,
description,
images: type === "image" ? [resourceUrl] : undefined,
video: type === "video" ? resourceUrl : undefined,
});
return NextResponse.json({
success: true,
url: resourceUrl,
asset,
});
} catch (error) {
if (error instanceof ApiError) {
return NextResponse.json({ success: false, error: error.message }, { status: error.status });
}
return NextResponse.json(
{ success: false, error: error instanceof Error ? error.message : "Asset create failed." },
{ status: 500 }
);
}
}

137
src/app/api/popi/media/upload/route.ts

@ -0,0 +1,137 @@
import { NextRequest, NextResponse } from "next/server";
import { requireLogin } from "@/app/api/_auth";
import { ApiError } from "@/app/api/_errors";
const POPI_MEDIA_UPLOAD_BASE64_PATH = "/api_client/media/uploadFileBase64";
type PopiserverResponse<T> = {
status?: string;
message?: string;
data?: T;
};
type PopiMedia = {
id?: number;
path?: string;
url?: string;
originPath?: string;
originUrl?: string;
thumbPath?: string;
thumbUrl?: string;
};
function getPopiBaseUrl(request: NextRequest): string {
const baseUrl =
process.env.POPIART_API_BASE_URL ||
process.env.NEXT_PUBLIC_APP_URL ||
request.nextUrl.origin;
return baseUrl.replace(/\/+$/, "");
}
function parseDataUrl(value: string): { mimeType: string; base64: string } | undefined {
const match = value.match(/^data:([^;,]+)?(?:;[^,]*)?;base64,(.*)$/);
if (!match) return undefined;
return {
mimeType: match[1] || "application/octet-stream",
base64: match[2],
};
}
function extensionForMimeType(mimeType: string): string {
const normalized = mimeType.toLowerCase();
if (normalized === "image/jpeg") return "jpg";
if (normalized === "image/png") return "png";
if (normalized === "image/webp") return "webp";
if (normalized === "image/gif") return "gif";
if (normalized === "video/mp4") return "mp4";
if (normalized === "video/webm") return "webm";
if (normalized === "audio/mpeg" || normalized === "audio/mp3") return "mp3";
if (normalized === "audio/wav" || normalized === "audio/x-wav") return "wav";
if (normalized === "model/gltf-binary") return "glb";
return normalized.split("/")[1]?.replace(/[^a-z0-9]/g, "") || "bin";
}
function mediaResourceUrl(media: PopiMedia): string | null {
return media.url || media.originUrl || media.path || media.originPath || null;
}
export async function POST(request: NextRequest) {
try {
const login = requireLogin(request);
const body = (await request.json().catch(() => null)) as {
data?: unknown;
fileName?: unknown;
contentType?: unknown;
} | null;
const value = typeof body?.data === "string" ? body.data : "";
const dataUrl = parseDataUrl(value);
if (!dataUrl) {
return NextResponse.json(
{ success: false, error: "Only base64 data URLs can be uploaded." },
{ status: 400 }
);
}
const extension = extensionForMimeType(
typeof body?.contentType === "string" ? body.contentType : dataUrl.mimeType
);
const rawFileName =
typeof body?.fileName === "string" && body.fileName.trim()
? body.fileName.trim()
: `canvas-media-${Date.now()}.${extension}`;
const fileName = rawFileName.includes(".") ? rawFileName : `${rawFileName}.${extension}`;
const upstream = await fetch(getPopiBaseUrl(request) + POPI_MEDIA_UPLOAD_BASE64_PATH, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${login.token}`,
token: login.token,
},
body: JSON.stringify({
data: dataUrl.base64,
fileName,
}),
cache: "no-store",
});
const payload = (await upstream.json().catch(() => null)) as PopiserverResponse<PopiMedia> | null;
if (!upstream.ok) {
return NextResponse.json(
{ success: false, error: payload?.message || "Popiserver media upload failed." },
{ status: upstream.status }
);
}
if (!payload || payload.status !== "0000" || !payload.data) {
return NextResponse.json(
{ success: false, error: payload?.message || "Popiserver media upload returned an error." },
{ status: 502 }
);
}
const resourceUrl = mediaResourceUrl(payload.data);
if (!resourceUrl) {
return NextResponse.json(
{ success: false, error: "Popiserver media upload response is missing resource URL." },
{ status: 502 }
);
}
return NextResponse.json({
success: true,
url: resourceUrl,
path: payload.data.path,
media: payload.data,
});
} catch (error) {
if (error instanceof ApiError) {
return NextResponse.json({ success: false, error: error.message }, { status: error.status });
}
return NextResponse.json(
{ success: false, error: error instanceof Error ? error.message : "Upload failed." },
{ status: 500 }
);
}
}

8
src/app/api/quickstart/propose/route.ts

@ -5,10 +5,12 @@ import {
getQuickstartMissingConfigError,
resolveQuickstartModelConfig,
} from "@/lib/quickstart/modelClient";
import { requireLogin } from "@/app/api/_auth";
import { resolveUserGatewayApiKey } from "@/app/api/_gatewayApiKey";
import { ApiError } from "@/app/api/_errors";
import { parseJSONFromResponse } from "@/lib/quickstart/validation";
import type { WorkflowProposal, WorkflowComplexity, NodeType } from "@/types";
import { isPopiProviderMode } from "@/lib/providerMode";
export const maxDuration = 60; // 1 minute timeout
@ -196,7 +198,10 @@ export async function POST(request: NextRequest) {
);
}
const newApiWGKey = await resolveUserGatewayApiKey(request);
const login = isPopiProviderMode() ? requireLogin(request) : null;
const newApiWGKey = isPopiProviderMode()
? null
: await resolveUserGatewayApiKey(request);
const modelConfig = resolveQuickstartModelConfig(request.headers, {
newApiWGApiKey: newApiWGKey,
});
@ -222,6 +227,7 @@ export async function POST(request: NextRequest) {
config: modelConfig,
maxOutputTokens: 8192,
requestId,
loginToken: login?.token,
});
const duration = Date.now() - startTime;

8
src/app/api/quickstart/route.ts

@ -7,8 +7,10 @@ import {
getQuickstartMissingConfigError,
resolveQuickstartModelConfig,
} from "@/lib/quickstart/modelClient";
import { requireLogin } from "@/app/api/_auth";
import { resolveUserGatewayApiKey } from "@/app/api/_gatewayApiKey";
import { ApiError } from "@/app/api/_errors";
import { isPopiProviderMode } from "@/lib/providerMode";
import {
validateWorkflowJSON,
repairWorkflowJSON,
@ -131,7 +133,10 @@ export async function POST(request: NextRequest) {
);
}
const newApiWGKey = await resolveUserGatewayApiKey(request);
const login = isPopiProviderMode() ? requireLogin(request) : null;
const newApiWGKey = isPopiProviderMode()
? null
: await resolveUserGatewayApiKey(request);
const modelConfig = resolveQuickstartModelConfig(request.headers, {
newApiWGApiKey: newApiWGKey,
});
@ -157,6 +162,7 @@ export async function POST(request: NextRequest) {
config: modelConfig,
maxOutputTokens: 16384, // Increased for complex workflows with many nodes
requestId,
loginToken: login?.token,
});
const duration = Date.now() - startTime;

2
src/components/ProjectSetupModal.tsx

@ -81,7 +81,7 @@ const getProviderIcon = (provider: ProviderType) => {
}
};
const DEFAULT_LLM_PROVIDER: LLMProvider = "newapiwg";
const DEFAULT_LLM_PROVIDER: LLMProvider = isPopiProviderMode() ? "popiserver" : "newapiwg";
const DEFAULT_LLM_TEMPERATURE = 0.7;
const DEFAULT_LLM_MAX_TOKENS = 8192;

5
src/components/composer/GenerationComposer.tsx

@ -107,9 +107,10 @@ const DEFAULT_VIDEO_MODEL = isPopiProviderMode()
: createNewApiWGDefaultVideoModel();
const DEFAULT_AUDIO_MODEL = createDefaultAudioModel();
const DEFAULT_PROMPT_TEXT_MODEL: SelectedModel = {
provider: "newapiwg",
provider: isPopiProviderMode() ? "popiserver" : "newapiwg",
modelId: DEFAULT_NEWAPIWG_LLM_MODEL_ID,
displayName: LLM_MODELS.newapiwg.find((model) => model.value === DEFAULT_NEWAPIWG_LLM_MODEL_ID)?.label || DEFAULT_NEWAPIWG_LLM_MODEL_ID,
displayName: (isPopiProviderMode() ? LLM_MODELS.popiserver : LLM_MODELS.newapiwg)
.find((model) => model.value === DEFAULT_NEWAPIWG_LLM_MODEL_ID)?.label || DEFAULT_NEWAPIWG_LLM_MODEL_ID,
capabilities: ["text-to-text"],
};

2
src/lib/llmModels.ts

@ -24,7 +24,7 @@ export const LLM_PROVIDERS: { value: LLMProvider; label: string }[] =
export const LLM_MODELS: Record<LLMProvider, LLMModelOption[]> = {
popiserver: [
{ value: "kimi-k2.6", label: "Kimi K2.6" },
{ value: DEFAULT_POPISERVER_LLM_MODEL_ID, label: "Kimi K2.6" },
],
newapiwg: [
{ value: "doubao-seed-2-0-lite-260428", label: "Doubao Seed 2.0 Lite" },

89
src/lib/quickstart/modelClient.ts

@ -1,9 +1,14 @@
import { GoogleGenAI } from "@google/genai";
import { DEFAULT_NEWAPIWG_LLM_MODEL_ID } from "@/lib/llmModels";
import { DEFAULT_NEWAPIWG_LLM_MODEL_ID, DEFAULT_POPISERVER_LLM_MODEL_ID } from "@/lib/llmModels";
import { DEFAULT_NEWAPIWG_BASE_URL } from "@/store/utils/localStorage";
import { logGatewayRequest, logGatewayResponse } from "@/app/api/_gatewayLogging";
import { isPopiProviderMode } from "@/lib/providerMode";
export type QuickstartModelConfig =
| {
provider: "popiserver";
model: string;
}
| {
provider: "newapiwg";
apiKey: string;
@ -26,6 +31,7 @@ type GenerateQuickstartTextOptions = {
config: QuickstartModelConfig;
maxOutputTokens: number;
requestId: string;
loginToken?: string | null;
fetchFn?: typeof fetch;
};
@ -34,6 +40,13 @@ export function resolveQuickstartModelConfig(
options: ResolveQuickstartModelConfigOptions = {}
): QuickstartModelConfig | null {
const env = options.env ?? process.env;
if (isPopiProviderMode()) {
return {
provider: "popiserver",
model: DEFAULT_POPISERVER_LLM_MODEL_ID,
};
}
const newApiWGKey = options.newApiWGApiKey;
if (newApiWGKey) {
return {
@ -57,13 +70,22 @@ export function resolveQuickstartModelConfig(
}
export function getQuickstartMissingConfigError(): string {
return "Gateway API key not found for current user.";
return isPopiProviderMode()
? "Please log in to use Popi Models."
: "Gateway API key not found for current user.";
}
export async function generateQuickstartText(
prompt: string,
options: GenerateQuickstartTextOptions
): Promise<string> {
if (options.config.provider === "popiserver") {
return generateWithPopiserver(prompt, {
...options,
config: options.config,
});
}
if (options.config.provider === "newapiwg") {
return generateWithNewApiWG(prompt, {
...options,
@ -81,6 +103,69 @@ function normalizeBaseUrl(baseUrl?: string | null): string {
return (baseUrl || DEFAULT_NEWAPIWG_BASE_URL).replace(/\/+$/, "");
}
function normalizePopiserverBaseUrl(env: EnvLike = process.env): string {
return (env.POPIART_API_BASE_URL || env.NEXT_PUBLIC_APP_URL || "").replace(/\/+$/, "");
}
async function generateWithPopiserver(
prompt: string,
{ config, maxOutputTokens, requestId, loginToken, fetchFn = fetch }: GenerateQuickstartTextOptions & {
config: Extract<QuickstartModelConfig, { provider: "popiserver" }>;
}
): Promise<string> {
const baseUrl = normalizePopiserverBaseUrl();
if (!baseUrl) {
throw new Error("POPIART_API_BASE_URL is not configured.");
}
if (!loginToken) {
throw new Error("Please log in to use Popi Models.");
}
const url = `${baseUrl}/api_client/anime/task/llmChat`;
const init: RequestInit = {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${loginToken}`,
token: loginToken,
},
body: JSON.stringify({
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: prompt },
],
model: config.model,
stream: false,
max_tokens: maxOutputTokens,
aiModelId: config.model === DEFAULT_POPISERVER_LLM_MODEL_ID ? 31 : undefined,
}),
};
const context = {
requestId,
model: config.model,
provider: config.provider,
};
const startedAt = Date.now();
logGatewayRequest("Calling quickstart PopiServer API", url, init, context);
const response = await fetchFn(url, init);
await logGatewayResponse("Quickstart PopiServer API response", url, init, response, context, startedAt);
const data = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(getNestedString(data, ["message"]) || `PopiServer API error: ${response.status}`);
}
if (!data || data.status !== "0000") {
throw new Error(getNestedString(data, ["message"]) || "PopiServer API returned an error");
}
const text = getNestedString(data, ["data", "choices", "0", "message", "content"]);
if (!text) {
throw new Error("No text in PopiServer response");
}
return text;
}
async function generateWithNewApiWG(
prompt: string,
{ config, maxOutputTokens, requestId, fetchFn = fetch }: GenerateQuickstartTextOptions & {

144
src/store/execution/simpleNodeExecutors.ts

@ -21,6 +21,123 @@ import type { NodeExecutionContext } from "./types";
import { resolveTextSourcesThroughRouters } from "@/store/utils/connectedInputs";
import { parseTextToArray } from "@/utils/arrayParser";
import { parseVarTags } from "@/utils/parseVarTags";
import { isPopiProviderMode } from "@/lib/providerMode";
const popiserverOutputAssetCache = new Map<string, Promise<string | null | undefined>>();
const USER_SESSION_STORAGE_KEY = "node-banana-user-session";
function isPopiserverMediaUrl(value: string): boolean {
if (value.startsWith("/media/") || value.startsWith("/media_private/")) return true;
try {
const url = new URL(value);
return url.pathname.startsWith("/media/") || url.pathname.startsWith("/media_private/");
} catch {
return false;
}
}
function isLocalMediaSource(value: string): boolean {
return value.startsWith("data:") || value.startsWith("blob:");
}
function extensionForMimeType(mimeType: string, kind: "image" | "video"): string {
const normalized = mimeType.toLowerCase();
if (normalized === "image/jpeg") return "jpg";
if (normalized === "image/png") return "png";
if (normalized === "image/webp") return "webp";
if (normalized === "image/gif") return "gif";
if (normalized === "video/mp4") return "mp4";
if (normalized === "video/webm") return "webm";
return kind === "video" ? "mp4" : "png";
}
async function blobToDataUrl(value: string): Promise<string> {
const response = await fetch(value);
if (!response.ok) {
throw new Error(`Failed to read local blob output: HTTP ${response.status}`);
}
const blob = await response.blob();
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = () => reject(new Error("FileReader error while reading local output"));
reader.onabort = () => reject(new Error("FileReader aborted while reading local output"));
reader.readAsDataURL(blob);
});
}
function mimeTypeFromDataUrl(value: string): string | null {
const match = value.match(/^data:([^;,]+)?(?:;[^,]*)?;base64,/);
return match?.[1] || null;
}
function getStoredLoginToken(): string | null {
if (typeof window === "undefined") return null;
try {
const stored = window.localStorage.getItem(USER_SESSION_STORAGE_KEY);
if (!stored) return null;
const parsed = JSON.parse(stored) as { state?: { token?: unknown } };
const token = parsed.state?.token;
return typeof token === "string" && token.trim() ? token.trim() : null;
} catch {
return null;
}
}
async function createPopiserverAssetForOutput(
kind: "image" | "video",
source: string | null | undefined,
title?: string
): Promise<string | null | undefined> {
if (!source || !isPopiProviderMode() || isPopiserverMediaUrl(source) || !isLocalMediaSource(source)) {
return source;
}
const token = getStoredLoginToken();
if (!token) return source;
const cacheKey = `${kind}:${source}`;
const cached = popiserverOutputAssetCache.get(cacheKey);
if (cached) return cached;
const uploadPromise = (async () => {
try {
const dataUrl = source.startsWith("blob:") ? await blobToDataUrl(source) : source;
const mimeType = mimeTypeFromDataUrl(dataUrl) || (kind === "video" ? "video/mp4" : "image/png");
const extension = extensionForMimeType(mimeType, kind);
const response = await fetch("/api/popi/asset/create", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
token,
},
body: JSON.stringify({
type: kind,
media: dataUrl,
title: title || "",
fileName: `canvas-output-${Date.now()}.${extension}`,
}),
});
const payload = (await response.json().catch(() => null)) as {
success?: boolean;
url?: unknown;
error?: string;
} | null;
if (!response.ok || !payload?.success || typeof payload.url !== "string") {
throw new Error(payload?.error || `Popiserver asset create failed: HTTP ${response.status}`);
}
return payload.url;
} catch (error) {
console.error("Failed to create popiserver asset for output:", error);
return source;
}
})();
popiserverOutputAssetCache.set(cacheKey, uploadPromise);
return uploadPromise;
}
/**
* Annotation node: receives upstream image as source, passes through if no annotations.
@ -221,9 +338,16 @@ export async function executeOutput(ctx: NodeExecutionContext): Promise<void> {
model3d ? "3d" :
undefined;
const uploadedImage = rawImage && !imageLooksLikeVideo
? await createPopiserverAssetForOutput("image", rawImage, outputNodeData.outputFilename)
: rawImage;
const uploadedVideo = videoContent
? await createPopiserverAssetForOutput("video", videoContent, outputNodeData.outputFilename)
: videoContent;
updateNodeData(node.id, {
image: imageContent,
video: videoContent,
image: uploadedImage ?? imageContent,
video: uploadedVideo,
audio: audioContent,
model3d: model3d ?? null,
contentType,
@ -251,15 +375,23 @@ export async function executeOutputGallery(ctx: NodeExecutionContext): Promise<v
const updates: Partial<OutputGalleryNodeData> = {};
const existingImages = new Set(galleryImages);
const newImages = images.filter((img) => !existingImages.has(img));
const newImages = await Promise.all(
images
.filter((img) => !existingImages.has(img))
.map((img) => createPopiserverAssetForOutput("image", img))
);
if (newImages.length > 0) {
updates.images = [...newImages, ...galleryImages];
updates.images = [...newImages.filter((img): img is string => !!img), ...galleryImages];
}
const existingVideos = new Set(galleryVideos);
const newVideos = videos.filter((v) => !existingVideos.has(v));
const newVideos = await Promise.all(
videos
.filter((v) => !existingVideos.has(v))
.map((video) => createPopiserverAssetForOutput("video", video))
);
if (newVideos.length > 0) {
updates.videos = [...newVideos, ...galleryVideos];
updates.videos = [...newVideos.filter((video): video is string => !!video), ...galleryVideos];
}
if (Object.keys(updates).length > 0) {

4
src/store/utils/buildApiHeaders.ts

@ -59,6 +59,10 @@ export function buildLlmHeaders(
"Content-Type": "application/json",
};
if (llmProvider === "popiserver") {
return headers;
}
if (llmProvider === "newapiwg") {
const newApiConfig = providerSettings.providers.newapiwg;
if (newApiConfig?.baseUrl) {

2
src/store/utils/nodeDefaults.ts

@ -245,7 +245,7 @@ export const createDefaultNodeData = (type: NodeType): WorkflowNodeData => {
inputImages: [],
inputVideos: [],
outputText: null,
provider: llmDefaults?.provider ?? "newapiwg",
provider: llmDefaults?.provider ?? (isPopiProviderMode() ? "popiserver" : "newapiwg"),
model: llmDefaults?.model ?? DEFAULT_NEWAPIWG_LLM_MODEL_ID,
temperature: llmDefaults?.temperature ?? 0.7,
maxTokens: llmDefaults?.maxTokens ?? 8192,

Loading…
Cancel
Save