Fix AI stream by returning stream.value directly from Server Actions.

createStreamableValue must be created and returned in the action itself; wrapping in { data } or a helper return caused production RSC serialization errors.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-10 22:24:57 +08:00
parent 3933905d66
commit 0f3bc2c50a
7 changed files with 102 additions and 109 deletions
+60 -61
View File
@@ -3,6 +3,8 @@ import { createOpenAI } from "@ai-sdk/openai";
import { createStreamableValue } from "ai/rsc";
import { ERROR_PREFIX } from "@/lib/constant";
export type AIStream = ReturnType<typeof createStreamableValue<string>>;
function getOpenAiBaseUrl(): string {
return (
process.env.OPENAI_BASE_URL ??
@@ -14,77 +16,74 @@ function getOpenAiBaseUrl(): string {
const model =
process.env.OPENAI_MODEL ?? "huihui_ai/gemma-4-abliterated:e4b";
export async function streamAIResponse(
system: string,
user: string,
): Promise<{ data?: ReturnType<typeof createStreamableValue<string>>["value"]; error?: string }> {
export function assertOpenAiKey(): string {
const apiKey = process.env.OPENAI_API_KEY?.trim();
if (!apiKey) {
return { error: "未配置 OPENAI_API_KEY,请在 .env.local 或 Docker env_file 中设置" };
throw new Error(
"未配置 OPENAI_API_KEY,请在 .env.local 或 Docker env_file 中设置",
);
}
return apiKey;
}
/** 在 Server Action 内 createStreamableValue 后调用,不可从子函数返回 stream.value */
export function pumpAIStream(
stream: AIStream,
system: string,
user: string,
): void {
const openai = createOpenAI({
apiKey,
apiKey: assertOpenAiKey(),
baseURL: getOpenAiBaseUrl(),
});
const stream = createStreamableValue<string>();
const { fullStream } = streamText({
temperature: 0.5,
model: openai(model),
messages: [
{ role: "system", content: system },
{ role: "user", content: user },
],
maxRetries: 0,
});
try {
const { fullStream } = streamText({
temperature: 0.5,
model: openai(model),
messages: [
{ role: "system", content: system },
{ role: "user", content: user },
],
maxRetries: 0,
});
let buffer = "";
let done = false;
const intervalId = setInterval(() => {
if (done && buffer.length === 0) {
clearInterval(intervalId);
stream.done();
return;
}
if (buffer.length <= 6) {
stream.update(buffer);
buffer = "";
} else {
const chunk = buffer.slice(0, 6);
buffer = buffer.slice(6);
stream.update(chunk);
}
}, 60);
let buffer = "";
let done = false;
const intervalId = setInterval(() => {
if (done && buffer.length === 0) {
clearInterval(intervalId);
stream.done();
return;
}
if (buffer.length <= 6) {
stream.update(buffer);
buffer = "";
} else {
const chunk = buffer.slice(0, 6);
buffer = buffer.slice(6);
stream.update(chunk);
}
}, 60);
(async () => {
for await (const part of fullStream) {
switch (part.type) {
case "text-delta":
buffer += part.textDelta;
break;
case "error": {
const err = part.error as { message?: string };
stream.update(ERROR_PREFIX + (err.message ?? String(part.error)));
break;
}
(async () => {
for await (const part of fullStream) {
switch (part.type) {
case "text-delta":
buffer += part.textDelta;
break;
case "error": {
const err = part.error as { message?: string };
stream.update(ERROR_PREFIX + (err.message ?? String(part.error)));
break;
}
}
})()
.catch((err) => {
const message = err instanceof Error ? err.message : String(err);
stream.update(ERROR_PREFIX + message);
})
.finally(() => {
done = true;
});
return { data: stream.value };
} catch (err) {
stream.done();
const message = err instanceof Error ? err.message : String(err);
return { error: message };
}
}
})()
.catch((err) => {
const message = err instanceof Error ? err.message : String(err);
stream.update(ERROR_PREFIX + message);
})
.finally(() => {
done = true;
});
}