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:
dekun
2026-06-10 22:39:09 +08:00
parent 0f3bc2c50a
commit dba0245cb1
13 changed files with 342 additions and 384 deletions
+145
View File
@@ -0,0 +1,145 @@
import {
calculateBazi,
formatBaziForPrompt,
} from "@/lib/calc/bazi";
import {
formatLiuyaoTimingForPrompt,
formatTimingForPrompt,
getTimingInfoWithLongitude,
} from "@/lib/calc/timing";
import {
extractChangeDetails,
extractZhangMingRen,
readGuaMarkdown,
} from "@/lib/content/zhouyi";
import {
BAZI_SYSTEM_PROMPT,
COMBINED_SYSTEM_PROMPT,
LIUYAO_SYSTEM_PROMPT,
} from "@/lib/prompts";
import type { AiRequestBody } from "@/lib/ai/types";
export async function buildAiMessages(
body: AiRequestBody,
): Promise<{ system: string; user: string }> {
switch (body.mode) {
case "liuyao": {
const input = body.payload;
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 = "";
}
return {
system: LIUYAO_SYSTEM_PROMPT,
user: `${timingText}
【卦象】
${input.guaTitle} ${input.guaResult} ${input.guaChange}
【问事】
${input.question}
${guaDetailText}`,
};
}
case "bazi": {
const { input, question, birthPlaceName } = body.payload;
const chart = calculateBazi(input);
const chartText = formatBaziForPrompt(chart);
return {
system: BAZI_SYSTEM_PROMPT,
user: `【出生时空】
出生地:${birthPlaceName}
【排盘信息】
${chartText}
【问事】
${question}`,
};
}
case "combined": {
const input = body.payload;
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");
}
}
return {
system: COMBINED_SYSTEM_PROMPT,
user: `【人和 · 八字排盘】
出生地:${input.birthPlaceName}
${chartText}
${timingText}
${hexagramText}
【问事】
${input.question}`,
};
}
}
throw new Error("未知 AI 模式");
}