feat: land page aggregate and phase7 document ai mainline
- 收口 page aggregate 读取、本地状态与命令客户端\n- 接入 phase7 document ai sidecar 与前端编排入口\n- 更新 architecture 与 design 状态迁移
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mockIsConvexEnabled = vi.fn();
|
||||
const mockGetAuthedConvexClient = vi.fn();
|
||||
const mockFetchDocumentAiOrchestratorConfig = vi.fn();
|
||||
|
||||
vi.mock("@/lib/convex/enabled", () => ({
|
||||
isConvexEnabled: mockIsConvexEnabled,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/convex/route", () => ({
|
||||
getAuthedConvexClient: mockGetAuthedConvexClient,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/server/document-ai-orchestrator", () => ({
|
||||
fetchDocumentAiOrchestratorConfig: mockFetchDocumentAiOrchestratorConfig,
|
||||
}));
|
||||
|
||||
describe("/api/ai-agent/document/config route", () => {
|
||||
beforeEach(() => {
|
||||
mockIsConvexEnabled.mockReset();
|
||||
mockGetAuthedConvexClient.mockReset();
|
||||
mockFetchDocumentAiOrchestratorConfig.mockReset();
|
||||
});
|
||||
|
||||
it("应返回 orchestrator config 能力面", async () => {
|
||||
mockIsConvexEnabled.mockReturnValue(true);
|
||||
mockGetAuthedConvexClient.mockResolvedValue({
|
||||
auth: {
|
||||
userId: "user-1",
|
||||
},
|
||||
});
|
||||
mockFetchDocumentAiOrchestratorConfig.mockResolvedValue({
|
||||
provider: "online",
|
||||
defaultModelKey: "gpt-5.4",
|
||||
defaultProfileId: "page_writer_ai_first",
|
||||
sessionEnabled: true,
|
||||
models: [{ key: "gpt-5.4", title: "GPT-5.4" }],
|
||||
profiles: [{ id: "page_writer_ai_first", title: "AI 主写" }],
|
||||
tools: [{ name: "doc_get", description: "读取当前页摘要" }],
|
||||
});
|
||||
|
||||
const { GET } = await import("./route");
|
||||
const response = await GET(new Request("http://127.0.0.1:3000/api/ai-agent/document/config"));
|
||||
const payload = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(mockFetchDocumentAiOrchestratorConfig).toHaveBeenCalledTimes(1);
|
||||
expect(payload.defaultModelKey).toBe("gpt-5.4");
|
||||
expect(payload.defaultProfileId).toBe("page_writer_ai_first");
|
||||
expect(payload.tools[0]?.name).toBe("doc_get");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { isConvexEnabled } from "@/lib/convex/enabled";
|
||||
import { getAuthedConvexClient } from "@/lib/convex/route";
|
||||
import { fetchDocumentAiOrchestratorConfig } from "@/lib/server/document-ai-orchestrator";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
let userId = "";
|
||||
if (isConvexEnabled()) {
|
||||
const { auth } = await getAuthedConvexClient();
|
||||
userId = auth.userId ?? "";
|
||||
}
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await fetchDocumentAiOrchestratorConfig({ request });
|
||||
return NextResponse.json(payload, {
|
||||
headers: {
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,9 @@ const mockIsConvexEnabled = vi.fn();
|
||||
const mockGetAuthedConvexClient = vi.fn();
|
||||
const mockStartHermesRun = vi.fn();
|
||||
const mockStreamHermesRunEvents = vi.fn();
|
||||
const mockFetchHermesStructuredToolResultFromMnoteWeb = vi.fn();
|
||||
const mockBuildDocumentBridgeContext = vi.fn();
|
||||
const mockExecuteRustBridgeTool = vi.fn();
|
||||
const mockStartDocumentAiOrchestratorRun = vi.fn();
|
||||
|
||||
vi.mock("@/lib/api-utils", async () => {
|
||||
const actual = await vi.importActual<typeof import("@/lib/api-utils")>("@/lib/api-utils");
|
||||
@@ -30,8 +32,16 @@ vi.mock("@/lib/ai-agent/hermes/bridge", () => ({
|
||||
streamHermesRunEvents: mockStreamHermesRunEvents,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/server/mnote-web-hermes", () => ({
|
||||
fetchHermesStructuredToolResultFromMnoteWeb: mockFetchHermesStructuredToolResultFromMnoteWeb,
|
||||
vi.mock("@/lib/documents/bridge", () => ({
|
||||
buildDocumentBridgeContext: mockBuildDocumentBridgeContext,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/documents/rust-runtime", () => ({
|
||||
executeRustBridgeTool: mockExecuteRustBridgeTool,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/server/document-ai-orchestrator", () => ({
|
||||
startDocumentAiOrchestratorRun: mockStartDocumentAiOrchestratorRun,
|
||||
}));
|
||||
|
||||
describe("/api/ai-agent/run route", () => {
|
||||
@@ -42,7 +52,9 @@ describe("/api/ai-agent/run route", () => {
|
||||
mockGetAuthedConvexClient.mockReset();
|
||||
mockStartHermesRun.mockReset();
|
||||
mockStreamHermesRunEvents.mockReset();
|
||||
mockFetchHermesStructuredToolResultFromMnoteWeb.mockReset();
|
||||
mockBuildDocumentBridgeContext.mockReset();
|
||||
mockExecuteRustBridgeTool.mockReset();
|
||||
mockStartDocumentAiOrchestratorRun.mockReset();
|
||||
});
|
||||
|
||||
it("应把 Hermes slash_run 完成事件恢复成结构化 tool_result", async () => {
|
||||
@@ -55,7 +67,7 @@ describe("/api/ai-agent/run route", () => {
|
||||
},
|
||||
options: {
|
||||
ai: {
|
||||
provider: "online",
|
||||
provider: "hermes",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -65,6 +77,28 @@ describe("/api/ai-agent/run route", () => {
|
||||
auth: {
|
||||
userId: "user-1",
|
||||
},
|
||||
client: { query: vi.fn(), mutation: vi.fn() },
|
||||
});
|
||||
mockBuildDocumentBridgeContext.mockResolvedValue({
|
||||
deploymentId: null,
|
||||
projectId: null,
|
||||
workspaceId: null,
|
||||
requestId: "req-1",
|
||||
traceId: "trace-1",
|
||||
actor: {
|
||||
actorType: "user",
|
||||
actorId: "user-1",
|
||||
sessionId: null,
|
||||
},
|
||||
source: {
|
||||
channel: "next-route",
|
||||
client: "wolai-frontend",
|
||||
},
|
||||
tenantId: null,
|
||||
authToken: null,
|
||||
idempotencyKey: null,
|
||||
validateOnly: false,
|
||||
dryRun: false,
|
||||
});
|
||||
mockStartHermesRun.mockResolvedValue({
|
||||
runId: "run-1",
|
||||
@@ -86,13 +120,19 @@ describe("/api/ai-agent/run route", () => {
|
||||
output: "已完成",
|
||||
});
|
||||
});
|
||||
mockFetchHermesStructuredToolResultFromMnoteWeb.mockResolvedValue({
|
||||
ok: true,
|
||||
parsed: {
|
||||
command: "rename_doc",
|
||||
params: {
|
||||
documentId: "doc-1",
|
||||
title: "AI 标题",
|
||||
mockExecuteRustBridgeTool.mockResolvedValue({
|
||||
plan: {
|
||||
kind: "tool",
|
||||
toolName: "slash_run",
|
||||
},
|
||||
result: {
|
||||
ok: true,
|
||||
parsed: {
|
||||
command: "rename_doc",
|
||||
params: {
|
||||
documentId: "doc-1",
|
||||
title: "AI 标题",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -106,13 +146,15 @@ describe("/api/ai-agent/run route", () => {
|
||||
const text = await response.text();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(mockFetchHermesStructuredToolResultFromMnoteWeb).toHaveBeenCalledWith(
|
||||
expect(mockExecuteRustBridgeTool).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
userId: "user-1",
|
||||
tool: "slash_run",
|
||||
argsJson: {
|
||||
toolName: "slash_run",
|
||||
args: {
|
||||
text: "/rename doc-1 AI 标题",
|
||||
},
|
||||
data: {
|
||||
source: "ai-agent-route",
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(text).toContain("event: tool_result");
|
||||
@@ -120,4 +162,223 @@ describe("/api/ai-agent/run route", () => {
|
||||
expect(text).toContain('"command":"rename_doc"');
|
||||
expect(text).toContain('"title":"AI 标题"');
|
||||
});
|
||||
|
||||
it("文档页 AI 请求应把 pageOptions 带入 Hermes instructions", async () => {
|
||||
mockSafeGetJsonBody.mockResolvedValue({
|
||||
stream: true,
|
||||
scope: "document",
|
||||
messages: [{ role: "user", content: "根据当前页面设置调整内容" }],
|
||||
context: {
|
||||
documentId: "doc-1",
|
||||
pageOptions: {
|
||||
wideLayout: true,
|
||||
smallText: true,
|
||||
layoutDensity: "compact",
|
||||
},
|
||||
},
|
||||
options: {
|
||||
ai: {
|
||||
provider: "hermes",
|
||||
},
|
||||
},
|
||||
});
|
||||
mockValidateRequestBody.mockReturnValue(null);
|
||||
mockIsConvexEnabled.mockReturnValue(true);
|
||||
mockGetAuthedConvexClient.mockResolvedValue({
|
||||
auth: {
|
||||
userId: "user-1",
|
||||
},
|
||||
});
|
||||
mockStartHermesRun.mockResolvedValue({
|
||||
runId: "run-2",
|
||||
});
|
||||
mockStreamHermesRunEvents.mockImplementation(async (_runId, onEvent) => {
|
||||
await onEvent({
|
||||
event: "run.completed",
|
||||
output: "已完成",
|
||||
});
|
||||
});
|
||||
|
||||
const { POST } = await import("./route");
|
||||
const response = await POST(
|
||||
new Request("http://127.0.0.1:3000/api/ai-agent/run", {
|
||||
method: "POST",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(mockStartHermesRun).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
instructions: expect.stringContaining("pageOptions="),
|
||||
}),
|
||||
);
|
||||
expect(mockStartHermesRun).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
instructions: expect.stringContaining('"wideLayout":true'),
|
||||
}),
|
||||
);
|
||||
expect(mockStartHermesRun).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
instructions: expect.stringContaining("editorRuntimePageOptions="),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("文档页在线流式请求应优先走 orchestrator", async () => {
|
||||
mockSafeGetJsonBody.mockResolvedValue({
|
||||
stream: true,
|
||||
scope: "document",
|
||||
messages: [{ role: "user", content: "总结当前页面" }],
|
||||
context: {
|
||||
documentId: "doc-1",
|
||||
documentBlocks: [],
|
||||
},
|
||||
options: {
|
||||
ai: {
|
||||
provider: "online",
|
||||
},
|
||||
},
|
||||
});
|
||||
mockValidateRequestBody.mockReturnValue(null);
|
||||
mockIsConvexEnabled.mockReturnValue(true);
|
||||
mockGetAuthedConvexClient.mockResolvedValue({
|
||||
auth: {
|
||||
userId: "user-1",
|
||||
},
|
||||
});
|
||||
mockStartDocumentAiOrchestratorRun.mockResolvedValue(
|
||||
new Response(
|
||||
'event: assistant_message\ndata: {"text":"来自 orchestrator"}\n\n',
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream; charset=utf-8",
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const { POST } = await import("./route");
|
||||
const response = await POST(
|
||||
new Request("http://127.0.0.1:3000/api/ai-agent/run", {
|
||||
method: "POST",
|
||||
}),
|
||||
);
|
||||
const text = await response.text();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(mockStartDocumentAiOrchestratorRun).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
userId: "user-1",
|
||||
}),
|
||||
);
|
||||
expect(mockStartHermesRun).not.toHaveBeenCalled();
|
||||
expect(text).toContain("event: ready");
|
||||
expect(text).toContain("来自 orchestrator");
|
||||
});
|
||||
|
||||
it("文档页在线请求应把 modelKey、profileId 和 sessionId 透传到 orchestrator", async () => {
|
||||
mockSafeGetJsonBody.mockResolvedValue({
|
||||
stream: true,
|
||||
scope: "document",
|
||||
messages: [{ role: "user", content: "总结当前页面" }],
|
||||
context: {
|
||||
documentId: "doc-1",
|
||||
documentBlocks: [],
|
||||
},
|
||||
options: {
|
||||
ai: {
|
||||
provider: "online",
|
||||
sessionId: "doc_ai:doc-1:session-1",
|
||||
modelKey: "gpt-5.3-codex",
|
||||
profileId: "page_writer_polish",
|
||||
},
|
||||
},
|
||||
});
|
||||
mockValidateRequestBody.mockReturnValue(null);
|
||||
mockIsConvexEnabled.mockReturnValue(true);
|
||||
mockGetAuthedConvexClient.mockResolvedValue({
|
||||
auth: {
|
||||
userId: "user-1",
|
||||
},
|
||||
});
|
||||
mockStartDocumentAiOrchestratorRun.mockResolvedValue(
|
||||
new Response(
|
||||
'event: assistant_message\ndata: {"text":"来自 orchestrator"}\n\n',
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream; charset=utf-8",
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const { POST } = await import("./route");
|
||||
const response = await POST(
|
||||
new Request("http://127.0.0.1:3000/api/ai-agent/run", {
|
||||
method: "POST",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(mockStartDocumentAiOrchestratorRun).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
userId: "user-1",
|
||||
payload: expect.objectContaining({
|
||||
options: expect.objectContaining({
|
||||
ai: expect.objectContaining({
|
||||
sessionId: "doc_ai:doc-1:session-1",
|
||||
modelKey: "gpt-5.3-codex",
|
||||
profileId: "page_writer_polish",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("orchestrator 失败时应自动回退 Hermes", async () => {
|
||||
mockSafeGetJsonBody.mockResolvedValue({
|
||||
stream: true,
|
||||
scope: "document",
|
||||
messages: [{ role: "user", content: "把标题改成回退标题" }],
|
||||
context: {
|
||||
documentId: "doc-1",
|
||||
},
|
||||
options: {
|
||||
ai: {
|
||||
provider: "online",
|
||||
},
|
||||
},
|
||||
});
|
||||
mockValidateRequestBody.mockReturnValue(null);
|
||||
mockIsConvexEnabled.mockReturnValue(true);
|
||||
mockGetAuthedConvexClient.mockResolvedValue({
|
||||
auth: {
|
||||
userId: "user-1",
|
||||
},
|
||||
});
|
||||
mockStartDocumentAiOrchestratorRun.mockRejectedValue(new Error("sidecar down"));
|
||||
mockStartHermesRun.mockResolvedValue({
|
||||
runId: "run-fallback",
|
||||
});
|
||||
mockStreamHermesRunEvents.mockImplementation(async (_runId, onEvent) => {
|
||||
await onEvent({
|
||||
event: "run.completed",
|
||||
output: "Hermes fallback",
|
||||
});
|
||||
});
|
||||
|
||||
const { POST } = await import("./route");
|
||||
const response = await POST(
|
||||
new Request("http://127.0.0.1:3000/api/ai-agent/run", {
|
||||
method: "POST",
|
||||
}),
|
||||
);
|
||||
const text = await response.text();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(mockStartDocumentAiOrchestratorRun).toHaveBeenCalledTimes(1);
|
||||
expect(mockStartHermesRun).toHaveBeenCalledTimes(1);
|
||||
expect(text).toContain("Hermes fallback");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,14 +17,18 @@ import {
|
||||
readHermesToolArgsFromEvent,
|
||||
readHermesToolResultFromEvent,
|
||||
} from "@/lib/ai-agent/hermes/tool-result-recovery";
|
||||
import { fetchHermesStructuredToolResultFromMnoteWeb } from "@/lib/server/mnote-web-hermes";
|
||||
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";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type AgentMessage = { role: "user" | "assistant"; content: string };
|
||||
type AgentScope = "global" | "mindmap" | "document" | "onlyoffice";
|
||||
type RawAiProvider = "online" | "local" | "ollama" | "codex" | "hermes";
|
||||
type RuntimeProvider = "hermes" | "codex";
|
||||
type RuntimeProvider = "agents" | "hermes" | "codex";
|
||||
type CodexMode = "chat" | "test" | "dev";
|
||||
|
||||
type RequestPayload = {
|
||||
@@ -48,6 +52,7 @@ type RequestPayload = {
|
||||
mindmapId?: string;
|
||||
selectedUids?: string[];
|
||||
documentBlocks?: unknown;
|
||||
pageOptions?: PageOptionsState;
|
||||
node?: unknown;
|
||||
subtree?: unknown;
|
||||
outline?: unknown;
|
||||
@@ -59,6 +64,8 @@ type RequestPayload = {
|
||||
provider?: RawAiProvider;
|
||||
model?: string;
|
||||
sessionId?: string;
|
||||
modelKey?: string;
|
||||
profileId?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -112,11 +119,46 @@ const clampSteps = (raw: unknown) => {
|
||||
return Math.max(MIN_AGENT_STEPS, Math.min(MAX_AGENT_STEPS, Math.floor(parsed)));
|
||||
};
|
||||
|
||||
const normalizeProvider = (raw: unknown): RuntimeProvider => {
|
||||
const normalizeProvider = (
|
||||
raw: unknown,
|
||||
scope: AgentScope,
|
||||
stream: boolean,
|
||||
): RuntimeProvider => {
|
||||
const value = String(raw ?? "").trim().toLowerCase();
|
||||
if (value === "online" && scope === "document" && stream) {
|
||||
return "agents";
|
||||
}
|
||||
return value === "codex" ? "codex" : "hermes";
|
||||
};
|
||||
|
||||
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 });
|
||||
};
|
||||
|
||||
const normalizeScope = (payload: RequestPayload): AgentScope => {
|
||||
const raw = String(payload.scope ?? "").trim();
|
||||
if (raw === "global" || raw === "mindmap" || raw === "document" || raw === "onlyoffice") {
|
||||
@@ -192,6 +234,22 @@ const buildHermesInstructions = (
|
||||
if (payload.context?.evidence !== undefined) {
|
||||
lines.push(serializeContextSnapshot("kernelEvidence", payload.context.evidence, 2500) ?? "kernelEvidence=provided");
|
||||
}
|
||||
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",
|
||||
);
|
||||
}
|
||||
if (attachments.length > 0) {
|
||||
lines.push(
|
||||
[
|
||||
@@ -237,6 +295,9 @@ const recoverStructuredHermesToolResult = async (input: {
|
||||
request: Request;
|
||||
payload: RequestPayload;
|
||||
userId: string;
|
||||
client: ReturnType<typeof getAuthedConvexClient> extends Promise<infer T>
|
||||
? T["client"]
|
||||
: never;
|
||||
tool: string;
|
||||
argsJson: Record<string, unknown> | null;
|
||||
fallbackRequestId: string;
|
||||
@@ -263,24 +324,41 @@ const recoverStructuredHermesToolResult = async (input: {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fetchHermesStructuredToolResultFromMnoteWeb({
|
||||
return buildDocumentBridgeContext({
|
||||
request: input.request,
|
||||
userId: input.userId,
|
||||
tool: input.tool,
|
||||
argsJson: input.argsJson,
|
||||
data,
|
||||
requestId: input.fallbackRequestId,
|
||||
traceId: input.fallbackTraceId,
|
||||
target: documentId
|
||||
? {
|
||||
pageId: documentId,
|
||||
blockId:
|
||||
input.tool === "doc_replace_range"
|
||||
? String(input.argsJson.blockId ?? "").trim() || null
|
||||
: null,
|
||||
}
|
||||
: null,
|
||||
}).catch(() => null);
|
||||
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);
|
||||
};
|
||||
|
||||
const streamHermesLegacyEvents = async ({
|
||||
@@ -619,13 +697,29 @@ export async function POST(request: Request) {
|
||||
return errorResponses.unauthorized();
|
||||
}
|
||||
|
||||
const provider = normalizeProvider(payload.options?.ai?.provider);
|
||||
const stream = payload.stream !== false;
|
||||
const scope = normalizeScope(payload);
|
||||
const provider = normalizeProvider(payload.options?.ai?.provider, scope, stream);
|
||||
if (provider === "codex") {
|
||||
return await runCodexBridge({ payload, request, stream });
|
||||
}
|
||||
|
||||
const scope = normalizeScope(payload);
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const maxSteps = clampSteps(payload.maxSteps);
|
||||
const instructions = buildHermesInstructions(payload, userId, scope, maxSteps);
|
||||
const sessionId = String(payload.options?.ai?.sessionId ?? "").trim() || null;
|
||||
|
||||
Reference in New Issue
Block a user