2026-01-10 23:08:56 +08:00
|
|
|
|
import { NextResponse } from "next/server";
|
2026-04-16 15:24:37 +08:00
|
|
|
|
import { safeGetJsonBody, errorResponses, validateRequestBody } from "@/lib/api-utils";
|
2026-01-21 18:21:10 +08:00
|
|
|
|
import {
|
|
|
|
|
|
DEFAULT_AGENT_MAX_STEPS,
|
|
|
|
|
|
MAX_AGENT_STEPS,
|
|
|
|
|
|
MIN_AGENT_STEPS,
|
|
|
|
|
|
} from "@/lib/constants";
|
2026-04-16 15:24:37 +08:00
|
|
|
|
import { isConvexEnabled } from "@/lib/convex/enabled";
|
|
|
|
|
|
import { getAuthedConvexClient } from "@/lib/convex/route";
|
|
|
|
|
|
import {
|
|
|
|
|
|
codexMessagesToPrompt,
|
|
|
|
|
|
findWorkspaceRoot,
|
|
|
|
|
|
startCodexJsonRun,
|
|
|
|
|
|
} from "@/lib/ai/codex/codexExec";
|
|
|
|
|
|
import { startHermesRun, streamHermesRunEvents, type HermesRunEvent } from "@/lib/ai-agent/hermes/bridge";
|
2026-04-22 05:57:06 +08:00
|
|
|
|
import {
|
|
|
|
|
|
readHermesToolArgsFromEvent,
|
|
|
|
|
|
readHermesToolResultFromEvent,
|
|
|
|
|
|
} from "@/lib/ai-agent/hermes/tool-result-recovery";
|
2026-04-23 07:38:34 +08:00
|
|
|
|
import { startDocumentAiOrchestratorRun } from "@/lib/server/document-ai-orchestrator";
|
|
|
|
|
|
import { buildDocumentBridgeContext } from "@/lib/documents/bridge";
|
|
|
|
|
|
import { executeRustBridgeTool } from "@/lib/documents/rust-runtime";
|
|
|
|
|
|
import type { PageOptionsState } from "@/types/page-options";
|
|
|
|
|
|
import { pickLeptosTiptapRuntimePageOptions } from "@/lib/documents/page-option-semantics";
|
2026-01-10 23:08:56 +08:00
|
|
|
|
|
|
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
|
|
|
2026-04-16 15:24:37 +08:00
|
|
|
|
type AgentMessage = { role: "user" | "assistant"; content: string };
|
2026-01-11 12:35:53 +08:00
|
|
|
|
type AgentScope = "global" | "mindmap" | "document" | "onlyoffice";
|
2026-04-16 15:24:37 +08:00
|
|
|
|
type RawAiProvider = "online" | "local" | "ollama" | "codex" | "hermes";
|
2026-04-23 07:38:34 +08:00
|
|
|
|
type RuntimeProvider = "agents" | "hermes" | "codex";
|
2026-04-13 19:21:42 +08:00
|
|
|
|
type CodexMode = "chat" | "test" | "dev";
|
2026-01-10 23:08:56 +08:00
|
|
|
|
|
|
|
|
|
|
type RequestPayload = {
|
|
|
|
|
|
stream?: boolean;
|
|
|
|
|
|
maxSteps?: number;
|
|
|
|
|
|
scope?: AgentScope;
|
|
|
|
|
|
messages: AgentMessage[];
|
2026-04-16 15:24:37 +08:00
|
|
|
|
attachments?: Array<{
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
title: string;
|
|
|
|
|
|
fileUrl: string;
|
|
|
|
|
|
mimeType?: string | null;
|
|
|
|
|
|
}>;
|
|
|
|
|
|
toolChoice?: {
|
|
|
|
|
|
mode: "auto" | "manual";
|
|
|
|
|
|
toolSets?: string[];
|
|
|
|
|
|
tools?: string[];
|
|
|
|
|
|
};
|
2026-01-10 23:08:56 +08:00
|
|
|
|
context?: {
|
|
|
|
|
|
documentId?: string;
|
|
|
|
|
|
mindmapId?: string;
|
|
|
|
|
|
selectedUids?: string[];
|
|
|
|
|
|
documentBlocks?: unknown;
|
2026-04-23 07:38:34 +08:00
|
|
|
|
pageOptions?: PageOptionsState;
|
2026-04-16 22:01:51 +08:00
|
|
|
|
node?: unknown;
|
|
|
|
|
|
subtree?: unknown;
|
|
|
|
|
|
outline?: unknown;
|
|
|
|
|
|
evidence?: unknown;
|
2026-01-10 23:08:56 +08:00
|
|
|
|
};
|
2026-04-16 15:24:37 +08:00
|
|
|
|
options?: {
|
|
|
|
|
|
searxng?: boolean;
|
|
|
|
|
|
ai?: {
|
|
|
|
|
|
provider?: RawAiProvider;
|
|
|
|
|
|
model?: string;
|
|
|
|
|
|
sessionId?: string;
|
2026-04-23 07:38:34 +08:00
|
|
|
|
modelKey?: string;
|
|
|
|
|
|
profileId?: string;
|
2026-04-16 15:24:37 +08:00
|
|
|
|
};
|
|
|
|
|
|
};
|
2026-01-10 23:08:56 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-16 15:24:37 +08:00
|
|
|
|
type LegacyStreamEvent =
|
|
|
|
|
|
| { type: "assistant_message"; data: { text: string } }
|
|
|
|
|
|
| { type: "tool_call"; data: { id: string; tool: string; args: Record<string, unknown> } }
|
|
|
|
|
|
| { type: "tool_result"; data: { id: string; tool: string; ok: boolean; ms: number; result: unknown } }
|
|
|
|
|
|
| { type: "completion"; data: { ok: true; text: string; steps: number } }
|
|
|
|
|
|
| { type: "error"; data: { ok: false; message: string } }
|
|
|
|
|
|
| { type: "codex_session"; data: { sessionId: string } };
|
2026-01-10 23:08:56 +08:00
|
|
|
|
|
|
|
|
|
|
const sseHeaders = {
|
|
|
|
|
|
"Content-Type": "text/event-stream; charset=utf-8",
|
|
|
|
|
|
"Cache-Control": "no-cache, no-transform",
|
|
|
|
|
|
Connection: "keep-alive",
|
|
|
|
|
|
"X-Accel-Buffering": "no",
|
|
|
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
|
|
const toSseFrame = (event: string, data: unknown) => {
|
|
|
|
|
|
const json = JSON.stringify(data ?? null);
|
|
|
|
|
|
return `event: ${event}\ndata: ${json}\n\n`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-16 15:24:37 +08:00
|
|
|
|
const makeRunId = () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
|
|
|
|
return crypto.randomUUID();
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
}
|
|
|
|
|
|
return `run_${Date.now()}_${Math.random().toString(16).slice(2, 10)}`;
|
|
|
|
|
|
};
|
2026-04-13 19:21:42 +08:00
|
|
|
|
|
2026-04-16 22:01:51 +08:00
|
|
|
|
const serializeContextSnapshot = (label: string, value: unknown, limit: number) => {
|
|
|
|
|
|
if (value === undefined) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
const text = JSON.stringify(value);
|
|
|
|
|
|
return `${label}=${text.slice(0, limit)}`;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return `${label}=provided`;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-16 15:24:37 +08:00
|
|
|
|
const clampSteps = (raw: unknown) => {
|
|
|
|
|
|
const parsed = Number(raw ?? DEFAULT_AGENT_MAX_STEPS);
|
|
|
|
|
|
if (!Number.isFinite(parsed)) return DEFAULT_AGENT_MAX_STEPS;
|
|
|
|
|
|
return Math.max(MIN_AGENT_STEPS, Math.min(MAX_AGENT_STEPS, Math.floor(parsed)));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-23 07:38:34 +08:00
|
|
|
|
const normalizeProvider = (
|
|
|
|
|
|
raw: unknown,
|
|
|
|
|
|
scope: AgentScope,
|
|
|
|
|
|
stream: boolean,
|
|
|
|
|
|
): RuntimeProvider => {
|
2026-04-16 15:24:37 +08:00
|
|
|
|
const value = String(raw ?? "").trim().toLowerCase();
|
2026-04-23 07:38:34 +08:00
|
|
|
|
if (value === "online" && scope === "document" && stream) {
|
|
|
|
|
|
return "agents";
|
|
|
|
|
|
}
|
2026-04-16 15:24:37 +08:00
|
|
|
|
return value === "codex" ? "codex" : "hermes";
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-23 07:38:34 +08:00
|
|
|
|
const proxySseResponse = (upstream: Response) => {
|
|
|
|
|
|
const encoder = new TextEncoder();
|
|
|
|
|
|
const body = new ReadableStream<Uint8Array>({
|
|
|
|
|
|
async start(controller) {
|
|
|
|
|
|
controller.enqueue(encoder.encode(toSseFrame("ready", { ok: true, requestId: makeRunId() })));
|
|
|
|
|
|
const reader = upstream.body?.getReader();
|
|
|
|
|
|
if (!reader) {
|
|
|
|
|
|
controller.close();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
while (true) {
|
|
|
|
|
|
const { value, done } = await reader.read();
|
|
|
|
|
|
if (done) break;
|
|
|
|
|
|
if (value) {
|
|
|
|
|
|
controller.enqueue(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
controller.close();
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return new Response(body, { headers: sseHeaders });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-16 15:24:37 +08:00
|
|
|
|
const normalizeScope = (payload: RequestPayload): AgentScope => {
|
|
|
|
|
|
const raw = String(payload.scope ?? "").trim();
|
|
|
|
|
|
if (raw === "global" || raw === "mindmap" || raw === "document" || raw === "onlyoffice") {
|
|
|
|
|
|
return raw;
|
|
|
|
|
|
}
|
|
|
|
|
|
return payload.context?.mindmapId ? "mindmap" : payload.context?.documentId ? "document" : "global";
|
2026-04-13 19:21:42 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const stripCodexModePrefix = (text: string): { mode: CodexMode | null; text: string } => {
|
2026-04-16 15:24:37 +08:00
|
|
|
|
const value = String(text ?? "");
|
|
|
|
|
|
const match = value.match(/^\s*#(chat|test|dev)\b[\s::\-–—]*/i);
|
|
|
|
|
|
if (!match) return { mode: null, text: value };
|
|
|
|
|
|
const mode = String(match[1] ?? "").toLowerCase() as CodexMode;
|
|
|
|
|
|
return { mode, text: value.slice(match[0].length).trimStart() };
|
2026-04-13 19:21:42 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const extractCodexModeFromMessages = (messages: AgentMessage[]) => {
|
2026-04-16 15:24:37 +08:00
|
|
|
|
const lastUser = [...messages].reverse().find((item) => item.role === "user")?.content ?? "";
|
2026-04-13 19:21:42 +08:00
|
|
|
|
const picked = stripCodexModePrefix(lastUser);
|
2026-04-16 15:24:37 +08:00
|
|
|
|
const cleanedMessages = messages.map((item) => {
|
|
|
|
|
|
if (item.role !== "user") return item;
|
|
|
|
|
|
const cleaned = stripCodexModePrefix(item.content);
|
|
|
|
|
|
return { ...item, content: cleaned.text };
|
2026-04-13 19:21:42 +08:00
|
|
|
|
});
|
2026-04-16 15:24:37 +08:00
|
|
|
|
return {
|
|
|
|
|
|
mode: picked.mode ?? "chat",
|
|
|
|
|
|
cleanedMessages,
|
|
|
|
|
|
};
|
2026-04-13 19:21:42 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-16 15:24:37 +08:00
|
|
|
|
const buildHermesInstructions = (
|
|
|
|
|
|
payload: RequestPayload,
|
|
|
|
|
|
userId: string,
|
|
|
|
|
|
scope: AgentScope,
|
|
|
|
|
|
maxSteps: number,
|
|
|
|
|
|
) => {
|
|
|
|
|
|
const attachments = Array.isArray(payload.attachments) ? payload.attachments.slice(0, 12) : [];
|
|
|
|
|
|
const selectedUids = Array.isArray(payload.context?.selectedUids)
|
|
|
|
|
|
? payload.context?.selectedUids.map((item) => String(item)).filter(Boolean).slice(0, 12)
|
|
|
|
|
|
: [];
|
|
|
|
|
|
|
|
|
|
|
|
const lines: string[] = [
|
|
|
|
|
|
"你当前运行在 mnote Web 前端的 Hermes bridge 后面。请始终使用简体中文。",
|
|
|
|
|
|
"当前前端已经收口为轻桥接层,不要再假设存在旧的前端 builtin registry、toolset 静态映射或 create*ServerTools 编排逻辑。",
|
|
|
|
|
|
"除非工具结果明确表明已完成写入,否则不要声称已经修改页面、思维导图或 OnlyOffice 文档。",
|
|
|
|
|
|
"不要把本机终端、文件系统或其他 Hermes 默认工具当成 mnote 业务真执行面。mnote 业务写入应视为独立桥能力。",
|
|
|
|
|
|
`当前调用用户:${userId}`,
|
|
|
|
|
|
`当前 scope:${scope}`,
|
|
|
|
|
|
`本轮最大步数提示:${maxSteps}`,
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
if (payload.context?.documentId) {
|
|
|
|
|
|
lines.push(`documentId=${String(payload.context.documentId).trim()}`);
|
2026-01-21 18:21:10 +08:00
|
|
|
|
}
|
2026-04-16 15:24:37 +08:00
|
|
|
|
if (payload.context?.mindmapId) {
|
|
|
|
|
|
lines.push(`mindmapId=${String(payload.context.mindmapId).trim()}`);
|
2026-01-21 18:21:10 +08:00
|
|
|
|
}
|
2026-04-16 15:24:37 +08:00
|
|
|
|
if (selectedUids.length > 0) {
|
|
|
|
|
|
lines.push(`selectedUids=${selectedUids.join(",")}`);
|
2026-01-10 23:08:56 +08:00
|
|
|
|
}
|
2026-04-16 15:24:37 +08:00
|
|
|
|
if (payload.context?.documentBlocks !== undefined) {
|
2026-04-16 22:01:51 +08:00
|
|
|
|
lines.push(serializeContextSnapshot("documentBlocksSnapshot", payload.context.documentBlocks, 4000) ?? "documentBlocksSnapshot=provided");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (payload.context?.node !== undefined) {
|
|
|
|
|
|
lines.push(serializeContextSnapshot("kernelNode", payload.context.node, 1800) ?? "kernelNode=provided");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (payload.context?.subtree !== undefined) {
|
|
|
|
|
|
lines.push(serializeContextSnapshot("kernelSubtree", payload.context.subtree, 5000) ?? "kernelSubtree=provided");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (payload.context?.outline !== undefined) {
|
|
|
|
|
|
lines.push(serializeContextSnapshot("kernelOutline", payload.context.outline, 2500) ?? "kernelOutline=provided");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (payload.context?.evidence !== undefined) {
|
|
|
|
|
|
lines.push(serializeContextSnapshot("kernelEvidence", payload.context.evidence, 2500) ?? "kernelEvidence=provided");
|
2026-04-13 19:21:42 +08:00
|
|
|
|
}
|
2026-04-23 07:38:34 +08:00
|
|
|
|
if (payload.context?.pageOptions !== undefined) {
|
|
|
|
|
|
lines.push(
|
|
|
|
|
|
serializeContextSnapshot(
|
|
|
|
|
|
"pageOptions",
|
|
|
|
|
|
payload.context.pageOptions,
|
|
|
|
|
|
2000,
|
|
|
|
|
|
) ?? "pageOptions=provided",
|
|
|
|
|
|
);
|
|
|
|
|
|
lines.push(
|
|
|
|
|
|
serializeContextSnapshot(
|
|
|
|
|
|
"editorRuntimePageOptions",
|
|
|
|
|
|
pickLeptosTiptapRuntimePageOptions(payload.context.pageOptions),
|
|
|
|
|
|
1200,
|
|
|
|
|
|
) ?? "editorRuntimePageOptions=provided",
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-04-16 15:24:37 +08:00
|
|
|
|
if (attachments.length > 0) {
|
|
|
|
|
|
lines.push(
|
|
|
|
|
|
[
|
|
|
|
|
|
"attachments:",
|
|
|
|
|
|
...attachments.map(
|
|
|
|
|
|
(item, index) =>
|
|
|
|
|
|
`${index + 1}. id=${String(item.id)} title=${String(item.title)} mime=${String(item.mimeType ?? "")} url=${String(item.fileUrl)}`,
|
|
|
|
|
|
),
|
|
|
|
|
|
].join("\n"),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-04-13 19:21:42 +08:00
|
|
|
|
|
2026-04-16 15:24:37 +08:00
|
|
|
|
if (payload.toolChoice?.mode === "manual") {
|
|
|
|
|
|
const tools = Array.isArray(payload.toolChoice.tools) ? payload.toolChoice.tools.filter(Boolean) : [];
|
|
|
|
|
|
const toolSets = Array.isArray(payload.toolChoice.toolSets) ? payload.toolChoice.toolSets.filter(Boolean) : [];
|
|
|
|
|
|
if (tools.length > 0 || toolSets.length > 0) {
|
|
|
|
|
|
lines.push(
|
|
|
|
|
|
`前端兼容提示:manual toolChoice tools=[${tools.join(", ")}] toolSets=[${toolSets.join(", ")}]. 这些只是旧前端兼容字段,不代表当前 Hermes 一定具备同名工具。`,
|
2026-04-13 19:21:42 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-01-10 23:08:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-16 15:24:37 +08:00
|
|
|
|
if (scope === "onlyoffice") {
|
|
|
|
|
|
lines.push("OnlyOffice 浏览器专属 client capability 仍在单独桥接;若当前后端没有明确写入结果,请直接说明限制,不要伪造选区修改。\n");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return lines.join("\n\n").trim();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const buildHermesInput = (messages: AgentMessage[]) => {
|
|
|
|
|
|
return messages
|
|
|
|
|
|
.slice(-50)
|
|
|
|
|
|
.filter((item) => item.role === "user" || item.role === "assistant")
|
|
|
|
|
|
.map((item) => ({ role: item.role, content: String(item.content ?? "") }));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-22 05:57:06 +08:00
|
|
|
|
type PendingHermesToolCall = {
|
|
|
|
|
|
preview: string;
|
|
|
|
|
|
argsJson: Record<string, unknown> | null;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const recoverStructuredHermesToolResult = async (input: {
|
|
|
|
|
|
request: Request;
|
|
|
|
|
|
payload: RequestPayload;
|
|
|
|
|
|
userId: string;
|
2026-04-23 07:38:34 +08:00
|
|
|
|
client: ReturnType<typeof getAuthedConvexClient> extends Promise<infer T>
|
|
|
|
|
|
? T["client"]
|
|
|
|
|
|
: never;
|
2026-04-22 05:57:06 +08:00
|
|
|
|
tool: string;
|
|
|
|
|
|
argsJson: Record<string, unknown> | null;
|
|
|
|
|
|
fallbackRequestId: string;
|
|
|
|
|
|
fallbackTraceId: string;
|
|
|
|
|
|
}): Promise<unknown | null> => {
|
|
|
|
|
|
if (!input.argsJson) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (
|
|
|
|
|
|
input.tool !== "slash_run" &&
|
|
|
|
|
|
input.tool !== "doc_insert_blocks" &&
|
|
|
|
|
|
input.tool !== "doc_replace_range"
|
|
|
|
|
|
) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const documentId = String(input.payload.context?.documentId ?? "").trim() || null;
|
|
|
|
|
|
const data =
|
|
|
|
|
|
input.tool === "slash_run"
|
|
|
|
|
|
? { source: "ai-agent-route" }
|
|
|
|
|
|
: input.payload.context?.documentBlocks ?? null;
|
|
|
|
|
|
|
|
|
|
|
|
if ((input.tool === "doc_insert_blocks" || input.tool === "doc_replace_range") && data == null) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 07:38:34 +08:00
|
|
|
|
return buildDocumentBridgeContext({
|
2026-04-22 05:57:06 +08:00
|
|
|
|
request: input.request,
|
2026-04-23 07:38:34 +08:00
|
|
|
|
workspaceId: null,
|
|
|
|
|
|
})
|
|
|
|
|
|
.then((context) =>
|
|
|
|
|
|
executeRustBridgeTool({
|
|
|
|
|
|
context: {
|
|
|
|
|
|
...context,
|
|
|
|
|
|
requestId: context.requestId || input.fallbackRequestId,
|
|
|
|
|
|
traceId: context.traceId || input.fallbackTraceId,
|
|
|
|
|
|
actor: {
|
|
|
|
|
|
...context.actor,
|
|
|
|
|
|
actorId: input.userId,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
toolName: input.tool,
|
|
|
|
|
|
invocationKind: "command",
|
|
|
|
|
|
args: input.argsJson,
|
|
|
|
|
|
data: (data && typeof data === "object" && !Array.isArray(data) ? data : { data }) as Record<
|
|
|
|
|
|
string,
|
|
|
|
|
|
unknown
|
|
|
|
|
|
>,
|
|
|
|
|
|
target: documentId
|
|
|
|
|
|
? {
|
|
|
|
|
|
pageId: documentId,
|
|
|
|
|
|
blockId:
|
|
|
|
|
|
input.tool === "doc_replace_range"
|
|
|
|
|
|
? String(input.argsJson.blockId ?? "").trim() || null
|
|
|
|
|
|
: null,
|
|
|
|
|
|
}
|
|
|
|
|
|
: null,
|
|
|
|
|
|
}),
|
|
|
|
|
|
)
|
|
|
|
|
|
.then((result) => result.result)
|
|
|
|
|
|
.catch(() => null);
|
2026-04-22 05:57:06 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-16 15:24:37 +08:00
|
|
|
|
const streamHermesLegacyEvents = async ({
|
|
|
|
|
|
messages,
|
|
|
|
|
|
instructions,
|
|
|
|
|
|
sessionId,
|
2026-04-22 05:57:06 +08:00
|
|
|
|
request,
|
|
|
|
|
|
payload,
|
|
|
|
|
|
userId,
|
2026-04-16 15:24:37 +08:00
|
|
|
|
onEvent,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
messages: AgentMessage[];
|
|
|
|
|
|
instructions: string;
|
|
|
|
|
|
sessionId: string | null;
|
2026-04-22 05:57:06 +08:00
|
|
|
|
request: Request;
|
|
|
|
|
|
payload: RequestPayload;
|
|
|
|
|
|
userId: string;
|
2026-04-16 15:24:37 +08:00
|
|
|
|
onEvent: (event: LegacyStreamEvent) => Promise<void> | void;
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
const input = buildHermesInput(messages);
|
|
|
|
|
|
const { runId } = await startHermesRun({
|
|
|
|
|
|
input,
|
|
|
|
|
|
instructions,
|
|
|
|
|
|
...(sessionId ? { session_id: sessionId } : {}),
|
2026-01-10 23:08:56 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-16 15:24:37 +08:00
|
|
|
|
const pendingToolIds = new Map<string, string[]>();
|
2026-04-22 05:57:06 +08:00
|
|
|
|
const pendingToolCalls = new Map<string, PendingHermesToolCall>();
|
2026-04-16 15:24:37 +08:00
|
|
|
|
let toolCount = 0;
|
|
|
|
|
|
let assistantBuffer = "";
|
|
|
|
|
|
let failureMessage = "";
|
|
|
|
|
|
let completed = false;
|
2026-01-10 23:08:56 +08:00
|
|
|
|
|
2026-04-16 15:24:37 +08:00
|
|
|
|
await streamHermesRunEvents(runId, async (event: HermesRunEvent) => {
|
|
|
|
|
|
if (event.event === "tool.started") {
|
|
|
|
|
|
const tool = String(event.tool ?? "").trim() || "unknown_tool";
|
|
|
|
|
|
const id = `hermes_${runId}_${++toolCount}`;
|
|
|
|
|
|
const queue = pendingToolIds.get(tool) ?? [];
|
|
|
|
|
|
queue.push(id);
|
|
|
|
|
|
pendingToolIds.set(tool, queue);
|
|
|
|
|
|
const preview = typeof event.preview === "string" ? event.preview : "";
|
2026-04-22 05:57:06 +08:00
|
|
|
|
pendingToolCalls.set(id, {
|
|
|
|
|
|
preview,
|
|
|
|
|
|
argsJson: readHermesToolArgsFromEvent(event, tool),
|
|
|
|
|
|
});
|
2026-04-16 15:24:37 +08:00
|
|
|
|
await onEvent({
|
|
|
|
|
|
type: "tool_call",
|
|
|
|
|
|
data: {
|
|
|
|
|
|
id,
|
|
|
|
|
|
tool,
|
|
|
|
|
|
args: preview ? { preview } : {},
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-01-10 23:08:56 +08:00
|
|
|
|
|
2026-04-16 15:24:37 +08:00
|
|
|
|
if (event.event === "tool.completed") {
|
|
|
|
|
|
const tool = String(event.tool ?? "").trim() || "unknown_tool";
|
|
|
|
|
|
const queue = pendingToolIds.get(tool) ?? [];
|
|
|
|
|
|
const id = queue.shift() ?? `hermes_${runId}_${toolCount}`;
|
|
|
|
|
|
pendingToolIds.set(tool, queue);
|
2026-04-22 05:57:06 +08:00
|
|
|
|
const pendingToolCall = pendingToolCalls.get(id) ?? null;
|
|
|
|
|
|
pendingToolCalls.delete(id);
|
|
|
|
|
|
const preview = pendingToolCall?.preview ?? "";
|
2026-04-16 15:24:37 +08:00
|
|
|
|
const duration = Number(event.duration ?? 0);
|
2026-04-22 05:57:06 +08:00
|
|
|
|
const structuredResultFromEvent = !Boolean(event.error) ? readHermesToolResultFromEvent(event) : null;
|
|
|
|
|
|
const recoveredResult =
|
|
|
|
|
|
structuredResultFromEvent ??
|
|
|
|
|
|
(await recoverStructuredHermesToolResult({
|
|
|
|
|
|
request,
|
|
|
|
|
|
payload,
|
|
|
|
|
|
userId,
|
|
|
|
|
|
tool,
|
|
|
|
|
|
argsJson: pendingToolCall?.argsJson ?? null,
|
|
|
|
|
|
fallbackRequestId: makeRunId(),
|
|
|
|
|
|
fallbackTraceId: makeRunId(),
|
|
|
|
|
|
}));
|
2026-04-16 15:24:37 +08:00
|
|
|
|
await onEvent({
|
|
|
|
|
|
type: "tool_result",
|
|
|
|
|
|
data: {
|
|
|
|
|
|
id,
|
|
|
|
|
|
tool,
|
|
|
|
|
|
ok: !Boolean(event.error),
|
|
|
|
|
|
ms: Number.isFinite(duration) ? Math.max(0, Math.round(duration * 1000)) : 0,
|
2026-04-22 05:57:06 +08:00
|
|
|
|
result:
|
|
|
|
|
|
recoveredResult ??
|
|
|
|
|
|
(preview ? { preview, error: Boolean(event.error) } : { error: Boolean(event.error) }),
|
2026-04-16 15:24:37 +08:00
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-01-10 23:08:56 +08:00
|
|
|
|
|
2026-04-16 15:24:37 +08:00
|
|
|
|
if (event.event === "message.delta") {
|
|
|
|
|
|
assistantBuffer += typeof event.delta === "string" ? event.delta : "";
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-01-10 23:08:56 +08:00
|
|
|
|
|
2026-04-16 15:24:37 +08:00
|
|
|
|
if (event.event === "run.failed") {
|
|
|
|
|
|
failureMessage = String(event.error ?? "Hermes run 失败");
|
|
|
|
|
|
await onEvent({ type: "error", data: { ok: false, message: failureMessage } });
|
|
|
|
|
|
completed = true;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-01-10 23:08:56 +08:00
|
|
|
|
|
2026-04-16 15:24:37 +08:00
|
|
|
|
if (event.event === "run.completed") {
|
|
|
|
|
|
const fallbackOutput = typeof event.output === "string" ? event.output : "";
|
|
|
|
|
|
const finalText = (assistantBuffer || fallbackOutput || "(无输出)").trim();
|
|
|
|
|
|
if (finalText) {
|
|
|
|
|
|
await onEvent({ type: "assistant_message", data: { text: finalText } });
|
2026-01-17 10:12:53 +08:00
|
|
|
|
}
|
2026-04-16 15:24:37 +08:00
|
|
|
|
await onEvent({
|
|
|
|
|
|
type: "completion",
|
|
|
|
|
|
data: { ok: true, text: finalText, steps: Math.max(1, toolCount || 1) },
|
|
|
|
|
|
});
|
|
|
|
|
|
completed = true;
|
2026-01-17 10:12:53 +08:00
|
|
|
|
}
|
2026-04-16 15:24:37 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!completed && !failureMessage) {
|
|
|
|
|
|
const finalText = (assistantBuffer || "(无输出)").trim();
|
|
|
|
|
|
if (finalText) {
|
|
|
|
|
|
await onEvent({ type: "assistant_message", data: { text: finalText } });
|
|
|
|
|
|
}
|
|
|
|
|
|
await onEvent({
|
|
|
|
|
|
type: "completion",
|
|
|
|
|
|
data: { ok: true, text: finalText, steps: Math.max(1, toolCount || 1) },
|
|
|
|
|
|
});
|
2026-01-17 10:12:53 +08:00
|
|
|
|
}
|
2026-04-16 15:24:37 +08:00
|
|
|
|
};
|
2026-01-17 10:12:53 +08:00
|
|
|
|
|
2026-04-16 15:24:37 +08:00
|
|
|
|
const runCodexBridge = async ({
|
|
|
|
|
|
payload,
|
|
|
|
|
|
request,
|
|
|
|
|
|
stream,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
payload: RequestPayload;
|
|
|
|
|
|
request: Request;
|
|
|
|
|
|
stream: boolean;
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
const { mode, cleanedMessages } = extractCodexModeFromMessages(payload.messages.slice(0, 50));
|
|
|
|
|
|
const workspaceRoot = await findWorkspaceRoot(process.cwd());
|
|
|
|
|
|
const sessionIdRaw = String(payload.options?.ai?.sessionId ?? "").trim();
|
|
|
|
|
|
const requestMode = mode;
|
|
|
|
|
|
const sys =
|
|
|
|
|
|
requestMode === "dev"
|
|
|
|
|
|
? "你当前处于 #dev 模式:可在工作区内读取/修改文件并执行命令,但只能影响当前工作区。请用简体中文输出。"
|
|
|
|
|
|
: requestMode === "test"
|
|
|
|
|
|
? "你当前处于 #test 模式:只做分析与回答,不要执行命令,不要修改文件,不要伪造工具执行。请用简体中文输出。"
|
|
|
|
|
|
: "你当前处于 #chat 模式:只聊天,不要执行命令,不要修改文件,不要输出 diff。请用简体中文输出。";
|
2026-01-10 23:08:56 +08:00
|
|
|
|
|
2026-04-16 15:24:37 +08:00
|
|
|
|
const buildPrompt = () => {
|
|
|
|
|
|
if (!sessionIdRaw) {
|
|
|
|
|
|
return codexMessagesToPrompt([{ role: "system", content: sys }, ...cleanedMessages]);
|
2026-01-17 10:12:53 +08:00
|
|
|
|
}
|
2026-04-16 15:24:37 +08:00
|
|
|
|
const lastUser = [...cleanedMessages].reverse().find((item) => item.role === "user")?.content ?? "";
|
|
|
|
|
|
const nextUserText = String(lastUser || "").trim();
|
|
|
|
|
|
if (!nextUserText) {
|
|
|
|
|
|
return codexMessagesToPrompt([{ role: "system", content: sys }, ...cleanedMessages]);
|
|
|
|
|
|
}
|
|
|
|
|
|
return codexMessagesToPrompt([
|
|
|
|
|
|
{ role: "system", content: sys },
|
|
|
|
|
|
{ role: "user", content: nextUserText },
|
|
|
|
|
|
]);
|
2026-01-17 10:12:53 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-10 23:08:56 +08:00
|
|
|
|
if (!stream) {
|
2026-04-16 15:24:37 +08:00
|
|
|
|
const run = startCodexJsonRun({
|
|
|
|
|
|
cwd: workspaceRoot,
|
|
|
|
|
|
sandbox: "workspace-write",
|
|
|
|
|
|
prompt: buildPrompt(),
|
|
|
|
|
|
model: null,
|
|
|
|
|
|
sessionId: sessionIdRaw || null,
|
|
|
|
|
|
});
|
|
|
|
|
|
const result = await run.done;
|
|
|
|
|
|
if (!result.ok) {
|
|
|
|
|
|
return NextResponse.json({ error: result.error }, { status: 500 });
|
|
|
|
|
|
}
|
|
|
|
|
|
return NextResponse.json({
|
|
|
|
|
|
text: result.text,
|
|
|
|
|
|
steps: 1,
|
|
|
|
|
|
events: [],
|
|
|
|
|
|
sessionId: result.threadId || sessionIdRaw || null,
|
|
|
|
|
|
});
|
2026-01-10 23:08:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const encoder = new TextEncoder();
|
2026-04-16 15:24:37 +08:00
|
|
|
|
let killActiveRun: (() => void) | null = null;
|
2026-01-10 23:08:56 +08:00
|
|
|
|
const body = new ReadableStream<Uint8Array>({
|
|
|
|
|
|
start(controller) {
|
|
|
|
|
|
const send = (event: string, data: unknown) => {
|
|
|
|
|
|
controller.enqueue(encoder.encode(toSseFrame(event, data)));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-11 12:35:53 +08:00
|
|
|
|
const requestId = makeRunId();
|
|
|
|
|
|
send("ready", { ok: true, requestId });
|
|
|
|
|
|
|
2026-04-16 15:24:37 +08:00
|
|
|
|
let sessionSent = false;
|
|
|
|
|
|
let assistantSent = false;
|
|
|
|
|
|
const toolStartAt = new Map<string, number>();
|
2026-01-11 12:35:53 +08:00
|
|
|
|
|
2026-04-16 15:24:37 +08:00
|
|
|
|
const stopRun = () => {
|
2026-01-10 23:08:56 +08:00
|
|
|
|
try {
|
2026-04-16 15:24:37 +08:00
|
|
|
|
killActiveRun?.();
|
2026-01-10 23:08:56 +08:00
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
}
|
2026-04-16 15:24:37 +08:00
|
|
|
|
};
|
2026-01-10 23:08:56 +08:00
|
|
|
|
|
2026-04-13 19:21:42 +08:00
|
|
|
|
const onAbort = () => {
|
2026-04-16 15:24:37 +08:00
|
|
|
|
stopRun();
|
2026-04-13 19:21:42 +08:00
|
|
|
|
};
|
|
|
|
|
|
try {
|
|
|
|
|
|
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-10 23:08:56 +08:00
|
|
|
|
(async () => {
|
2026-04-16 15:24:37 +08:00
|
|
|
|
const run = startCodexJsonRun({
|
|
|
|
|
|
cwd: workspaceRoot,
|
|
|
|
|
|
sandbox: "workspace-write",
|
|
|
|
|
|
prompt: buildPrompt(),
|
|
|
|
|
|
model: null,
|
|
|
|
|
|
sessionId: sessionIdRaw || null,
|
|
|
|
|
|
onJsonLine: (line) => {
|
|
|
|
|
|
if (line.type === "thread.started") {
|
|
|
|
|
|
const sid = String((line as { thread_id?: string }).thread_id ?? "").trim();
|
|
|
|
|
|
if (sid && !sessionSent) {
|
|
|
|
|
|
sessionSent = true;
|
|
|
|
|
|
send("codex_session", { sessionId: sid });
|
2026-04-13 19:21:42 +08:00
|
|
|
|
}
|
2026-04-16 15:24:37 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (line.type === "item.started" && (line as { item?: { type?: string; id?: string; command?: string } }).item?.type === "command_execution") {
|
|
|
|
|
|
const item = (line as { item?: { type?: string; id?: string; command?: string } }).item;
|
|
|
|
|
|
const id = String(item?.id ?? "").trim();
|
|
|
|
|
|
const command = String(item?.command ?? "");
|
|
|
|
|
|
if (!id) return;
|
|
|
|
|
|
toolStartAt.set(id, Date.now());
|
|
|
|
|
|
send("tool_call", { id, tool: "codex_command", args: { command } });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (line.type === "item.completed" && (line as { item?: { type?: string; id?: string; exit_code?: number; aggregated_output?: string } }).item?.type === "command_execution") {
|
|
|
|
|
|
const item = (line as { item?: { type?: string; id?: string; exit_code?: number; aggregated_output?: string } }).item;
|
|
|
|
|
|
const id = String(item?.id ?? "").trim();
|
|
|
|
|
|
if (!id) return;
|
|
|
|
|
|
const startedAt = toolStartAt.get(id) ?? Date.now();
|
|
|
|
|
|
const ms = Math.max(0, Date.now() - startedAt);
|
|
|
|
|
|
const exitCode = Number(item?.exit_code ?? 0);
|
|
|
|
|
|
send("tool_result", {
|
|
|
|
|
|
id,
|
|
|
|
|
|
tool: "codex_command",
|
|
|
|
|
|
ok: exitCode === 0,
|
|
|
|
|
|
ms,
|
|
|
|
|
|
result: {
|
|
|
|
|
|
exitCode,
|
|
|
|
|
|
output: String(item?.aggregated_output ?? ""),
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (line.type === "item.completed" && (line as { item?: { type?: string; text?: string } }).item?.type === "agent_message") {
|
|
|
|
|
|
const item = (line as { item?: { type?: string; text?: string } }).item;
|
|
|
|
|
|
const text = String(item?.text ?? "").trim();
|
|
|
|
|
|
if (text) {
|
|
|
|
|
|
assistantSent = true;
|
|
|
|
|
|
send("assistant_message", { text });
|
2026-01-11 12:35:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-10 23:08:56 +08:00
|
|
|
|
},
|
|
|
|
|
|
});
|
2026-04-16 15:24:37 +08:00
|
|
|
|
killActiveRun = run.kill;
|
2026-01-10 23:08:56 +08:00
|
|
|
|
|
2026-04-16 15:24:37 +08:00
|
|
|
|
const result = await run.done;
|
2026-01-10 23:08:56 +08:00
|
|
|
|
if (!result.ok) {
|
|
|
|
|
|
send("error", { ok: false, message: result.error });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-04-16 15:24:37 +08:00
|
|
|
|
|
|
|
|
|
|
if (result.threadId && !sessionSent) {
|
|
|
|
|
|
sessionSent = true;
|
|
|
|
|
|
send("codex_session", { sessionId: result.threadId });
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!assistantSent && result.text) {
|
|
|
|
|
|
assistantSent = true;
|
|
|
|
|
|
send("assistant_message", { text: result.text });
|
|
|
|
|
|
}
|
|
|
|
|
|
send("completion", { ok: true, text: result.text, steps: 1 });
|
2026-01-10 23:08:56 +08:00
|
|
|
|
})()
|
2026-04-16 15:24:37 +08:00
|
|
|
|
.catch((error) => {
|
|
|
|
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
|
|
|
|
send("error", { ok: false, message });
|
2026-01-10 23:08:56 +08:00
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
2026-04-13 19:21:42 +08:00
|
|
|
|
try {
|
|
|
|
|
|
request.signal?.removeEventListener("abort", onAbort);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
}
|
2026-04-16 15:24:37 +08:00
|
|
|
|
stopRun();
|
2026-01-10 23:08:56 +08:00
|
|
|
|
controller.close();
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
2026-04-13 19:21:42 +08:00
|
|
|
|
cancel() {
|
2026-04-16 15:24:37 +08:00
|
|
|
|
try {
|
|
|
|
|
|
killActiveRun?.();
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return new Response(body, { headers: sseHeaders });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export async function POST(request: Request) {
|
|
|
|
|
|
const payload = await safeGetJsonBody<RequestPayload>(request);
|
|
|
|
|
|
if (!payload) {
|
|
|
|
|
|
return errorResponses.badRequest("请求体不能为空");
|
|
|
|
|
|
}
|
|
|
|
|
|
const validationError = validateRequestBody(payload, ["messages"] as const);
|
|
|
|
|
|
if (validationError) return validationError;
|
|
|
|
|
|
if (!Array.isArray(payload.messages) || payload.messages.length === 0) {
|
|
|
|
|
|
return errorResponses.badRequest("缺少 messages");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let userId = "";
|
|
|
|
|
|
if (isConvexEnabled()) {
|
|
|
|
|
|
const { auth } = await getAuthedConvexClient();
|
|
|
|
|
|
userId = auth.userId ?? "";
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!userId) {
|
|
|
|
|
|
return errorResponses.unauthorized();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const stream = payload.stream !== false;
|
2026-04-23 07:38:34 +08:00
|
|
|
|
const scope = normalizeScope(payload);
|
|
|
|
|
|
const provider = normalizeProvider(payload.options?.ai?.provider, scope, stream);
|
2026-04-16 15:24:37 +08:00
|
|
|
|
if (provider === "codex") {
|
|
|
|
|
|
return await runCodexBridge({ payload, request, stream });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 07:38:34 +08:00
|
|
|
|
if (provider === "agents") {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const upstream = await startDocumentAiOrchestratorRun({
|
|
|
|
|
|
request,
|
|
|
|
|
|
userId,
|
|
|
|
|
|
payload,
|
|
|
|
|
|
});
|
|
|
|
|
|
return proxySseResponse(upstream);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn(
|
|
|
|
|
|
"document ai orchestrator 不可用,回退 Hermes",
|
|
|
|
|
|
error instanceof Error ? error.message : String(error),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-16 15:24:37 +08:00
|
|
|
|
const maxSteps = clampSteps(payload.maxSteps);
|
|
|
|
|
|
const instructions = buildHermesInstructions(payload, userId, scope, maxSteps);
|
|
|
|
|
|
const sessionId = String(payload.options?.ai?.sessionId ?? "").trim() || null;
|
|
|
|
|
|
|
|
|
|
|
|
if (!stream) {
|
|
|
|
|
|
const events: LegacyStreamEvent[] = [];
|
|
|
|
|
|
await streamHermesLegacyEvents({
|
|
|
|
|
|
messages: payload.messages,
|
|
|
|
|
|
instructions,
|
|
|
|
|
|
sessionId,
|
2026-04-22 05:57:06 +08:00
|
|
|
|
request,
|
|
|
|
|
|
payload,
|
|
|
|
|
|
userId,
|
2026-04-16 15:24:37 +08:00
|
|
|
|
onEvent: (event) => {
|
|
|
|
|
|
events.push(event);
|
|
|
|
|
|
},
|
|
|
|
|
|
}).catch((error) => {
|
|
|
|
|
|
throw error instanceof Error ? error : new Error(String(error));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const completion = [...events].reverse().find((event) => event.type === "completion") as Extract<LegacyStreamEvent, { type: "completion" }> | undefined;
|
|
|
|
|
|
const assistant = [...events].reverse().find((event) => event.type === "assistant_message") as Extract<LegacyStreamEvent, { type: "assistant_message" }> | undefined;
|
|
|
|
|
|
return NextResponse.json({
|
|
|
|
|
|
text: completion?.data.text ?? assistant?.data.text ?? "",
|
|
|
|
|
|
steps: completion?.data.steps ?? Math.max(1, events.filter((event) => event.type === "tool_call").length || 1),
|
|
|
|
|
|
events,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const encoder = new TextEncoder();
|
|
|
|
|
|
const body = new ReadableStream<Uint8Array>({
|
|
|
|
|
|
start(controller) {
|
|
|
|
|
|
const send = (event: string, data: unknown) => {
|
|
|
|
|
|
controller.enqueue(encoder.encode(toSseFrame(event, data)));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
send("ready", { ok: true, requestId: makeRunId() });
|
|
|
|
|
|
|
|
|
|
|
|
const ping = setInterval(() => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
controller.enqueue(encoder.encode(`: ping ${Date.now()}\n\n`));
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 15_000);
|
|
|
|
|
|
|
|
|
|
|
|
(async () => {
|
|
|
|
|
|
await streamHermesLegacyEvents({
|
|
|
|
|
|
messages: payload.messages,
|
|
|
|
|
|
instructions,
|
|
|
|
|
|
sessionId,
|
2026-04-22 05:57:06 +08:00
|
|
|
|
request,
|
|
|
|
|
|
payload,
|
|
|
|
|
|
userId,
|
2026-04-16 15:24:37 +08:00
|
|
|
|
onEvent: (event) => {
|
|
|
|
|
|
send(event.type, event.data ?? null);
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
})()
|
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
|
|
|
|
send("error", { ok: false, message });
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
clearInterval(ping);
|
|
|
|
|
|
controller.close();
|
|
|
|
|
|
});
|
2026-04-13 19:21:42 +08:00
|
|
|
|
},
|
2026-01-10 23:08:56 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return new Response(body, { headers: sseHeaders });
|
|
|
|
|
|
}
|