feat: land page aggregate and phase7 document ai mainline
- 收口 page aggregate 读取、本地状态与命令客户端\n- 接入 phase7 document ai sidecar 与前端编排入口\n- 更新 architecture 与 design 状态迁移
This commit is contained in:
@@ -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