Replace RSC streaming with /api/ai Route Handler for Docker reliability.
Server Actions + createStreamableValue kept failing in production; fetch-based text stream avoids RSC serialization issues and shows readable error messages. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,34 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { createStreamableValue } from "ai/rsc";
|
||||
import { pumpAIStream } from "@/lib/ai/stream";
|
||||
import {
|
||||
calculateBazi,
|
||||
formatBaziForPrompt,
|
||||
type BaziInput,
|
||||
} from "@/lib/calc/bazi";
|
||||
import { BAZI_SYSTEM_PROMPT } from "@/lib/prompts";
|
||||
|
||||
export async function getBaziAnswer(
|
||||
input: BaziInput,
|
||||
question: string,
|
||||
birthPlaceName: string,
|
||||
) {
|
||||
const chart = calculateBazi(input);
|
||||
const chartText = formatBaziForPrompt(chart);
|
||||
|
||||
const stream = createStreamableValue<string>();
|
||||
pumpAIStream(
|
||||
stream,
|
||||
BAZI_SYSTEM_PROMPT,
|
||||
`【出生时空】
|
||||
出生地:${birthPlaceName}
|
||||
|
||||
【排盘信息】
|
||||
${chartText}
|
||||
|
||||
【问事】
|
||||
${question}`,
|
||||
);
|
||||
return stream.value;
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { createStreamableValue } from "ai/rsc";
|
||||
import { pumpAIStream } from "@/lib/ai/stream";
|
||||
import {
|
||||
calculateBazi,
|
||||
formatBaziForPrompt,
|
||||
type BaziInput,
|
||||
} from "@/lib/calc/bazi";
|
||||
import {
|
||||
formatTimingForPrompt,
|
||||
getTimingInfoWithLongitude,
|
||||
} from "@/lib/calc/timing";
|
||||
import { COMBINED_SYSTEM_PROMPT } from "@/lib/prompts";
|
||||
import {
|
||||
extractChangeDetails,
|
||||
extractZhangMingRen,
|
||||
readGuaMarkdown,
|
||||
} from "@/lib/content/zhouyi";
|
||||
|
||||
export interface CombinedInput {
|
||||
birth: BaziInput;
|
||||
birthPlaceName: string;
|
||||
currentPlaceName: string;
|
||||
currentLongitude: number;
|
||||
calcDate: string;
|
||||
calcTime: string;
|
||||
question: string;
|
||||
hexagram?: {
|
||||
guaMark: string;
|
||||
guaTitle: string;
|
||||
guaResult: string;
|
||||
guaChange: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function getCombinedAnswer(input: CombinedInput) {
|
||||
const chart = calculateBazi(input.birth);
|
||||
const chartText = formatBaziForPrompt(chart);
|
||||
const { timing, trueSolarTime } = getTimingInfoWithLongitude(
|
||||
input.calcDate,
|
||||
input.calcTime,
|
||||
input.currentLongitude,
|
||||
);
|
||||
const timingText = [
|
||||
formatTimingForPrompt(timing, input.currentPlaceName, input.currentLongitude),
|
||||
`真太阳时:${trueSolarTime}`,
|
||||
].join("\n");
|
||||
|
||||
let hexagramText = "";
|
||||
if (input.hexagram) {
|
||||
const { guaMark, guaTitle, guaResult, guaChange } = input.hexagram;
|
||||
try {
|
||||
const guaDetail = await readGuaMarkdown(guaMark);
|
||||
const explain = extractZhangMingRen(guaDetail) ?? "";
|
||||
const changeList = extractChangeDetails(guaDetail, guaChange, guaTitle);
|
||||
hexagramText = [
|
||||
"",
|
||||
"【卦象 · 可选六爻】",
|
||||
`${guaTitle} ${guaResult} ${guaChange}`,
|
||||
explain,
|
||||
changeList.join("\n"),
|
||||
].join("\n");
|
||||
} catch {
|
||||
hexagramText = [
|
||||
"",
|
||||
"【卦象 · 可选六爻】",
|
||||
`${input.hexagram.guaTitle} ${input.hexagram.guaResult} ${input.hexagram.guaChange}`,
|
||||
].join("\n");
|
||||
}
|
||||
}
|
||||
|
||||
const stream = createStreamableValue<string>();
|
||||
pumpAIStream(
|
||||
stream,
|
||||
COMBINED_SYSTEM_PROMPT,
|
||||
`【人和 · 八字排盘】
|
||||
出生地:${input.birthPlaceName}
|
||||
${chartText}
|
||||
|
||||
${timingText}
|
||||
${hexagramText}
|
||||
|
||||
【问事】
|
||||
${input.question}`,
|
||||
);
|
||||
return stream.value;
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { createStreamableValue } from "ai/rsc";
|
||||
import { pumpAIStream } from "@/lib/ai/stream";
|
||||
import {
|
||||
formatLiuyaoTimingForPrompt,
|
||||
getTimingInfoWithLongitude,
|
||||
} from "@/lib/calc/timing";
|
||||
import {
|
||||
extractChangeDetails,
|
||||
extractZhangMingRen,
|
||||
readGuaMarkdown,
|
||||
} from "@/lib/content/zhouyi";
|
||||
import { LIUYAO_SYSTEM_PROMPT } from "@/lib/prompts";
|
||||
|
||||
export interface LiuyaoInput {
|
||||
question: string;
|
||||
calcDate: string;
|
||||
calcTime: string;
|
||||
locationName: string;
|
||||
longitude: number;
|
||||
guaMark: string;
|
||||
guaTitle: string;
|
||||
guaResult: string;
|
||||
guaChange: string;
|
||||
}
|
||||
|
||||
export async function getLiuyaoAnswer(input: LiuyaoInput) {
|
||||
const { timing, trueSolarTime } = getTimingInfoWithLongitude(
|
||||
input.calcDate,
|
||||
input.calcTime,
|
||||
input.longitude,
|
||||
);
|
||||
const timingText = formatLiuyaoTimingForPrompt(
|
||||
timing,
|
||||
trueSolarTime,
|
||||
input.locationName,
|
||||
input.longitude,
|
||||
);
|
||||
|
||||
let guaDetailText = "";
|
||||
try {
|
||||
const guaDetail = await readGuaMarkdown(input.guaMark);
|
||||
const explain = extractZhangMingRen(guaDetail) ?? "";
|
||||
const changeList = extractChangeDetails(
|
||||
guaDetail,
|
||||
input.guaChange,
|
||||
input.guaTitle,
|
||||
);
|
||||
guaDetailText = [explain, changeList.join("\n")].filter(Boolean).join("\n");
|
||||
} catch {
|
||||
guaDetailText = "";
|
||||
}
|
||||
|
||||
const stream = createStreamableValue<string>();
|
||||
pumpAIStream(
|
||||
stream,
|
||||
LIUYAO_SYSTEM_PROMPT,
|
||||
`${timingText}
|
||||
|
||||
【卦象】
|
||||
${input.guaTitle} ${input.guaResult} ${input.guaChange}
|
||||
|
||||
【问事】
|
||||
${input.question}
|
||||
|
||||
${guaDetailText}`,
|
||||
);
|
||||
return stream.value;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { streamText } from "ai";
|
||||
import { createOpenAI } from "@ai-sdk/openai";
|
||||
import { buildAiMessages } from "@/lib/ai/build-messages";
|
||||
import {
|
||||
getOpenAiApiKey,
|
||||
getOpenAiBaseUrl,
|
||||
getOpenAiModel,
|
||||
} from "@/lib/ai/config";
|
||||
import type { AiRequestBody } from "@/lib/ai/types";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body = (await req.json()) as AiRequestBody;
|
||||
if (!body?.mode || !body.payload) {
|
||||
return new Response("请求格式错误", { status: 400 });
|
||||
}
|
||||
|
||||
const { system, user } = await buildAiMessages(body);
|
||||
const openai = createOpenAI({
|
||||
apiKey: getOpenAiApiKey(),
|
||||
baseURL: getOpenAiBaseUrl(),
|
||||
});
|
||||
|
||||
const result = streamText({
|
||||
temperature: 0.5,
|
||||
model: openai(getOpenAiModel()),
|
||||
messages: [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: user },
|
||||
],
|
||||
maxRetries: 0,
|
||||
});
|
||||
|
||||
return result.toTextStreamResponse();
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return new Response(message, {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { getLiuyaoAnswer } from "@/app/actions/liuyao";
|
||||
|
||||
/** 兼容旧版 divination 组件签名 */
|
||||
export async function getAnswer(
|
||||
prompt: string,
|
||||
guaMark: string,
|
||||
guaTitle: string,
|
||||
guaResult: string,
|
||||
guaChange: string,
|
||||
) {
|
||||
const now = new Date();
|
||||
const calcDate = now.toISOString().slice(0, 10);
|
||||
const calcTime = `${now.getHours().toString().padStart(2, "0")}:${now.getMinutes().toString().padStart(2, "0")}`;
|
||||
|
||||
return getLiuyaoAnswer({
|
||||
question: prompt,
|
||||
calcDate,
|
||||
calcTime,
|
||||
locationName: "未指定",
|
||||
longitude: 120,
|
||||
guaMark,
|
||||
guaTitle,
|
||||
guaResult,
|
||||
guaChange,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user