Fix AI streaming, learn images, and full city regions.

Remove use server from stream helper to fix RSC errors; support OPENAI_API_BASE alias; render HTML tables via rehype-raw with gua-image API; expand regions to 356 prefecture-level cities.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-10 22:04:17 +08:00
parent 698a20a1d4
commit 3933905d66
11 changed files with 1565 additions and 77 deletions
+23 -14
View File
@@ -1,27 +1,33 @@
"use server";
import { streamText } from "ai";
import { createOpenAI } from "@ai-sdk/openai";
import { createStreamableValue } from "ai/rsc";
import { ERROR_PREFIX } from "@/lib/constant";
function getOpenAiBaseUrl(): string {
return (
process.env.OPENAI_BASE_URL ??
process.env.OPENAI_API_BASE ??
"https://op.bz121.com/v1"
);
}
const model =
process.env.OPENAI_MODEL ?? "huihui_ai/gemma-4-abliterated:e4b";
const openai = createOpenAI({
baseURL: process.env.OPENAI_BASE_URL ?? "https://op.bz121.com/v1",
});
const STREAM_INTERVAL = 60;
const MAX_SIZE = 6;
export async function streamAIResponse(
system: string,
user: string,
): Promise<{ data?: ReturnType<typeof createStreamableValue<string>>["value"]; error?: string }> {
if (!process.env.OPENAI_API_KEY?.trim()) {
const apiKey = process.env.OPENAI_API_KEY?.trim();
if (!apiKey) {
return { error: "未配置 OPENAI_API_KEY,请在 .env.local 或 Docker env_file 中设置" };
}
const openai = createOpenAI({
apiKey,
baseURL: getOpenAiBaseUrl(),
});
const stream = createStreamableValue<string>();
try {
@@ -43,15 +49,15 @@ export async function streamAIResponse(
stream.done();
return;
}
if (buffer.length <= MAX_SIZE) {
if (buffer.length <= 6) {
stream.update(buffer);
buffer = "";
} else {
const chunk = buffer.slice(0, MAX_SIZE);
buffer = buffer.slice(MAX_SIZE);
const chunk = buffer.slice(0, 6);
buffer = buffer.slice(6);
stream.update(chunk);
}
}, STREAM_INTERVAL);
}, 60);
(async () => {
for await (const part of fullStream) {
@@ -67,7 +73,10 @@ export async function streamAIResponse(
}
}
})()
.catch(console.error)
.catch((err) => {
const message = err instanceof Error ? err.message : String(err);
stream.update(ERROR_PREFIX + message);
})
.finally(() => {
done = true;
});
+8 -5
View File
@@ -41,11 +41,14 @@ export function extractChangeDetails(
return changeList;
}
guaChange
.split(":")[1]
.trim()
.split(",")
.forEach((change) => {
const changePart = guaChange.includes(":")
? guaChange.split(":").slice(1).join(":").trim()
: guaChange.trim();
if (!changePart) {
return changeList;
}
changePart.split(",").forEach((change) => {
const detail = guaDetail
.match(`(\\*\\*${change}變卦[\\s\\S]*?)(?=${guaTitle}|$)`)?.[1]
?.replaceAll("\n\n", "\n");
+896 -4
View File
File diff suppressed because it is too large Load Diff