You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

291 lines
10 KiB

import { defineEventHandler, getQuery, getRouterParam, readBody, setResponseHeaders } from "h3";
import { R } from "#server/utils/response";
import { getCurrentUser, getConfigGlobal, getConfigUser } from "#server/utils/context";
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { type LanguageModel, streamText, stepCountIs } from "ai";
import { getSessionByIdAndUser, getMessagesBySession, getMaxSortOrder, saveMessage, truncateMessagesAfter, deleteMessage, getMessageById, updateMessageContent, touchSession, countAssistantMessages } from "#server/service/agent/session";
import { getAgentToolsForChat } from "#server/service/agent-tool";
import { getTempTokenFromCookie } from "#server/service/agent/temp-token";
import { checkRateLimit, incrementRateLimit } from "#server/service/agent/rate-limit";
import { getModelWithProviderById, getSystemModelWithProviderById } from "#server/service/llm";
import { generateSessionTitle } from "#server/service/agent/title";
import log4js from "logger";
const logger = log4js.getLogger("APP");
function resolveModel(
provider: {
name: string;
apiKey: string | null;
baseUrl: string | null;
parseMode: string;
},
modelId: string,
): LanguageModel {
const baseUrl = provider.baseUrl?.replace(/\/+$/, "") || undefined;
if (provider.parseMode === "anthropic") {
throw createError({
statusCode: 400,
statusMessage: "Anthropic 解析模式暂不支持流式对话,请使用 OpenAI 兼容模式",
});
}
const openaiCompatible = createOpenAICompatible({
name: provider.name,
apiKey: provider.apiKey || undefined,
baseURL: baseUrl || "https://api.openai.com/v1",
});
return openaiCompatible(modelId) as LanguageModel;
}
export default defineEventHandler(async (event) => {
const user = await getCurrentUser(event);
const tempToken = getTempTokenFromCookie(event);
if (!user && !tempToken) {
throw createError({ statusCode: 401, statusMessage: "请先登录或创建临时会话" });
}
const body = await readBody(event);
const { sessionId, content, editMessageId, regenerate } = body as {
sessionId: string;
content: string;
editMessageId?: string;
regenerate?: boolean;
};
if (!sessionId || !content || typeof content !== "string" || content.trim().length === 0) {
throw createError({ statusCode: 400, statusMessage: "参数无效" });
}
const session = await getSessionByIdAndUser(sessionId, user?.id ?? null, tempToken);
if (!session) {
throw createError({ statusCode: 404, statusMessage: "会话不存在" });
}
const ip = getRequestIP(event, { xForwardedFor: true }) || "unknown";
if (!user) {
const rateLimit = checkRateLimit(sessionId, ip);
if (rateLimit.blocked) {
setResponseHeaders(event, {
"X-RateLimit-Session-Remaining": String(rateLimit.sessionRemaining),
"X-RateLimit-Ip-Remaining": String(rateLimit.ipRemaining),
});
throw createError({
statusCode: 429,
statusMessage: rateLimit.sessionRemaining === 0 ? "当前会话已达回复上限,请登录后继续" : "今日回复次数已达上限,请登录或明日再试",
});
}
setResponseHeaders(event, {
"X-RateLimit-Session-Remaining": String(rateLimit.sessionRemaining),
"X-RateLimit-Ip-Remaining": String(rateLimit.ipRemaining),
});
}
let modelRow;
if (user) {
const preferredModelId = await getConfigUser(event, "preferredLlmModelId");
const targetModelId = session.modelId ?? preferredModelId;
if (!targetModelId) {
throw createError({ statusCode: 400, statusMessage: "未配置模型,请先在设置中选择模型" });
}
modelRow = await getModelWithProviderById(targetModelId, user.id);
} else {
if (!session.modelId) {
throw createError({ statusCode: 400, statusMessage: "系统未配置默认模型" });
}
modelRow = await getSystemModelWithProviderById(session.modelId);
}
if (!modelRow) {
throw createError({ statusCode: 404, statusMessage: "模型不存在或无权访问" });
}
const { model, provider } = modelRow;
if (!provider.apiKey) {
throw createError({ statusCode: 400, statusMessage: "供应商未配置 API Key" });
}
const systemPrompt = await getConfigGlobal("agentSystemPrompt");
const titleModelId = await getConfigGlobal("agentTitleModelId");
let publicToolSlugs: string[] = [];
if (!user) {
const raw = await getConfigGlobal("agentPublicToolSlugs");
if (raw && Array.isArray(raw)) {
publicToolSlugs = raw as string[];
}
}
const { tools, approvalConfig } = await getAgentToolsForChat({
userId: user?.id ?? null,
userRole: user?.role ?? null,
enableTools: session.enableTools === 1,
publicToolSlugs,
});
let historyMessages = await getMessagesBySession(sessionId, { limit: 100 });
let userMessage;
let shouldGenerateTitle = false;
if (editMessageId) {
const editMsg = await getMessageById(editMessageId);
if (!editMsg || editMsg.sessionId !== sessionId) {
throw createError({ statusCode: 400, statusMessage: "编辑的消息不存在" });
}
await truncateMessagesAfter(sessionId, editMsg.sortOrder);
await updateMessageContent(editMessageId, content.trim());
historyMessages = await getMessagesBySession(sessionId, { limit: 100 });
userMessage = editMsg;
} else if (regenerate) {
const lastAssistant = historyMessages.filter((m) => m.role === "assistant").pop();
if (lastAssistant) {
await deleteMessage(lastAssistant.id);
historyMessages = await getMessagesBySession(sessionId, { limit: 100 });
}
const lastUser = historyMessages.filter((m) => m.role === "user").pop();
if (lastUser) {
await updateMessageContent(lastUser.id, content.trim());
userMessage = lastUser;
} else {
const sortOrder = (await getMaxSortOrder(sessionId)) + 1;
userMessage = await saveMessage({
sessionId,
role: "user",
content: content.trim(),
sortOrder,
});
shouldGenerateTitle = true;
}
} else {
const sortOrder = (await getMaxSortOrder(sessionId)) + 1;
userMessage = await saveMessage({
sessionId,
role: "user",
content: content.trim(),
sortOrder,
});
const assistantCount = await countAssistantMessages(sessionId);
if (assistantCount === 0) {
shouldGenerateTitle = true;
}
}
const modelMessages = [
...historyMessages
.filter((m) => m.id !== userMessage.id || editMessageId || regenerate)
.map((m) => ({
role: m.role as "user" | "assistant",
content: m.content,
})),
{ role: "user" as const, content: content.trim() },
];
const languageModel = resolveModel(provider, model.modelId);
logger.info(
"[%s] [AGENT-CHAT] userId=%s sessionId=%s modelId=%d thinking=%s tools=%s edit=%s regen=%s",
event.context.requestId ?? "-",
user?.id ?? "temp",
sessionId,
model.id,
session.enableThinking === 1 ? "on" : "off",
Object.keys(tools).length > 0 ? "on" : "off",
editMessageId ? "yes" : "no",
regenerate ? "yes" : "no",
);
const result = streamText({
model: languageModel,
system: systemPrompt || undefined,
messages: modelMessages,
maxOutputTokens: model.maxTokens || undefined,
...(Object.keys(tools).length > 0
? { tools, stopWhen: stepCountIs(8), toolApproval: approvalConfig }
: {}),
...(session.enableThinking === 1
? {
providerOptions: {
openaiCompatible: { reasoningEffort: "high" },
},
}
: {}),
onError: (errorData) => {
const errMsg = errorData?.error instanceof Error
? errorData.error.message
: String(errorData?.error ?? "未知错误");
logger.error("[%s] [AGENT-CHAT] streamText error: %s", event.context.requestId ?? "-", errMsg);
},
onFinish: async ({ finishReason, usage, steps, text: assistantText }) => {
logger.info(
"[%s] [AGENT-CHAT] finished: reason=%s steps=%d inputTokens=%d outputTokens=%d",
event.context.requestId ?? "-",
finishReason,
steps.length,
usage?.inputTokens ?? 0,
usage?.outputTokens ?? 0,
);
const parts: Array<Record<string, unknown>> = [];
for (const step of steps) {
for (const r of step.reasoning) {
if (r.type === "reasoning" && r.text) {
parts.push({ id: `p_${parts.length}`, type: "reasoning", text: r.text });
}
}
const toolResultMap = new Map(step.toolResults.map((tr) => [tr.toolCallId, tr.output]));
for (const tc of step.toolCalls) {
parts.push({
id: `p_${parts.length}`,
type: "tool-call",
toolCallId: tc.toolCallId,
toolName: tc.toolName,
args: tc.input,
result: toolResultMap.get(tc.toolCallId),
state: "result",
});
}
if (step.text) {
parts.push({ id: `p_${parts.length}`, type: "text", text: step.text });
}
}
const assistantSortOrder = (await getMaxSortOrder(sessionId)) + 1;
await saveMessage({
sessionId,
role: "assistant",
content: assistantText,
parts: parts.length > 0 ? JSON.stringify(parts) : null,
modelId: model.id,
inputTokens: usage?.inputTokens ?? null,
outputTokens: usage?.outputTokens ?? null,
sortOrder: assistantSortOrder,
});
await touchSession(sessionId);
if (!user) {
incrementRateLimit(sessionId, ip);
const rateLimit = checkRateLimit(sessionId, ip);
setResponseHeaders(event, {
"X-RateLimit-Session-Remaining": String(rateLimit.sessionRemaining),
"X-RateLimit-Ip-Remaining": String(rateLimit.ipRemaining),
});
}
if (shouldGenerateTitle && titleModelId && assistantText.trim().length > 0) {
generateSessionTitle(sessionId, content.trim(), assistantText, titleModelId, user?.id ?? null).catch((e) => { logger.error("[AGENT-CHAT] title gen failed: %s", e?.message ?? e); });
}
},
});
const response = result.toUIMessageStreamResponse({
sendReasoning: true,
headers: {
"X-User-Message-Id": userMessage.id,
},
});
return response;
});