-
Notifications
You must be signed in to change notification settings - Fork 45
Expand file tree
/
Copy pathroute.ts
More file actions
165 lines (146 loc) · 5.66 KB
/
route.ts
File metadata and controls
165 lines (146 loc) · 5.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import { generateText } from "ai";
import { getModel, requiresApiKey, type AIProvider } from "@/lib/ai/models";
import { createGlmFlashModel } from "@/lib/ai/providers/glm";
// 允许流式响应最长30秒
export const maxDuration = 30;
import type { UIMessage, TextUIPart } from "ai";
interface SuggestionsRequest {
messages: UIMessage[];
pageContext?: {
title?: string;
description?: string;
slug?: string;
};
provider?: AIProvider;
apiKey?: string;
}
export async function POST(req: Request) {
try {
const {
messages,
pageContext,
provider = "intern",
apiKey,
}: SuggestionsRequest = await req.json();
// 如果需要,验证 API 密钥
if (requiresApiKey(provider) && (!apiKey || apiKey.trim() === "")) {
return Response.json(
{ error: "需要 API 密钥 (API key is required)" },
{ status: 400 },
);
}
// 模型选择策略:
// - 若用户选了自己的 Provider(openai/gemini),用用户的模型
// - 否则(默认 intern)优先用 GLM-4-Flash(免费且快速),若 ZHIPU_API_KEY 未配置则回退到 intern
let model;
if (provider !== "intern") {
// 用户自选模型(openai / gemini)
model = getModel(provider, apiKey);
} else if (process.env.ZHIPU_API_KEY) {
// 默认使用智谱 GLM-4-Flash(免费轻量)
model = createGlmFlashModel();
} else {
// 兜底:仍使用 intern
model = getModel("intern");
}
const isWelcomeRequest = messages.length === 0;
let prompt = "";
if (isWelcomeRequest) {
// 欢迎页面的初始动态建议
const contextInfo = [
pageContext?.title ? `标题: ${pageContext.title}` : "",
pageContext?.description ? `描述: ${pageContext.description}` : "",
]
.filter(Boolean)
.join("\n");
prompt = `请根据以下当前页面的上下文信息,生成4个引导新手用户提问的建议框内容。\n\n上下文:\n${contextInfo || "未知页面"}\n\n只返回纯JSON数组,包含4个对象,格式如:\n[{"title":"总结本文","label":"内容要点","action":"请帮我总结一下文章主要内容"}]\n其中title简短明确,label为右上角浅色标签,action为点击后自动发送的提问语句。`;
} else {
// 普通的跟进提问建议
// 只取最后一条用户消息,减少 token 消耗
const lastUserMsg = messages
.filter((m) => m.role === "user")
.slice(-1)[0];
const lastText =
(Array.isArray(lastUserMsg?.parts)
? lastUserMsg.parts
.filter((p): p is TextUIPart => p.type === "text")
.map((p) => p.text)
.join(" ")
: (lastUserMsg as unknown as { content?: string })?.content) ?? "";
// 语言检测:简单判断是否包含中文字符
const isChinese = /[\u4e00-\u9fa5]/.test(lastText);
prompt = isChinese
? `用户问:"${lastText}"。给出3个简短中文追问(每个不超过15字),直接返回JSON数组,例如:["问题1","问题2","问题3"]`
: `User asked: "${lastText}". Suggest 3 short follow-up questions (max 10 words each). Return a JSON array only, e.g. ["Q1","Q2","Q3"]`;
}
const { text } = await generateText({
model,
prompt,
});
let questions: unknown[] = [];
try {
// 尝试解析 JSON
// 清理可能存在的 Markdown 代码块标记
let cleanedText = text
.replace(/```json/gi, "")
.replace(/```/g, "")
.trim();
// 修复大模型可能生成的中文引号
cleanedText = cleanedText.replace(/“/g, '"').replace(/”/g, '"');
// 尝试仅提取数组部分,防止 AI 返回了前缀描述文本
const arrayMatch = cleanedText.match(/\[[\s\S]*\]/);
if (arrayMatch) {
cleanedText = arrayMatch[0];
}
questions = JSON.parse(cleanedText);
} catch (e) {
console.error("解析建议 JSON 失败:", e, "原始文本:", text);
if (isWelcomeRequest) {
// 把报错原因和原始文本暴露出来方便我调试
return Response.json({
questions: [],
debugError: String(e),
debugText: text,
});
} else {
// 如果解析失败,尝试通过正则提取引号中的内容(兼容中英文引号)
const fallbackMatches = text.match(/(?:["“])([^"”]+)(?:["”])/g);
if (fallbackMatches && fallbackMatches.length > 0) {
questions = fallbackMatches
.map((m) => m.replace(/["“”]/g, "").trim())
.filter((line) => line.length > 0);
} else {
// 如果连引号都没有,尝试按行分割兜底
questions = text
.split("\n")
.map((line) =>
line
.replace(/^\d+\.\s*/, "")
.replace(/[`"“”]/g, "")
.trim(),
)
.filter(
(line) =>
line.length > 0 &&
!line.startsWith("json") &&
!line.startsWith("[") &&
!line.startsWith("]"),
);
}
}
}
// 确保返回的是数组
if (!Array.isArray(questions)) {
questions = [];
}
// 对于跟进建议最多返回 3 个,对于欢迎建议最多返回 4 个
const maxCount = isWelcomeRequest ? 4 : 3;
return Response.json({ questions: questions.slice(0, maxCount) });
} catch (error) {
console.error("建议 API 错误 (Suggestions API error):", error);
return Response.json(
{ error: "无法生成建议 (Failed to generate suggestions)" },
{ status: 500 },
);
}
}