feat(ui,docs): Agent 流式侧栏 + 结果区/MCP 点/确认条 + 官网文案

- 侧栏消费 Agent SSE:时间线边 append、正文 ▌、停止按钮
- 「在结果区打开」不抢编辑器 focus;MCP 活跃点;写操作确认条
- 文档/落地页补充 Ask|Agent 与 npx @rlwj-lab/opsxdb-mcp(不宣传默认可写)
This commit is contained in:
logan committed 2026-10-15 13:00:58 +08:00
1 parent 2336c9433c
commit 979efb606d
9 files changed
+580 -92

No files matched your search

+143
View File
@@ -552,6 +552,149 @@ export async function callAiAgent(payload: {
return post<AiAgentRes>("/api/ai/agent", payload);
}
export type AiAgentQueryResult = {
columns: string[];
rows: Record<string, unknown>[];
rowCount: number;
sql?: string;
tool?: string;
};
export type AiAgentStepExt = AiAgentStep & {
pending?: boolean;
needConfirm?: boolean;
confirmSql?: string;
result?: AiAgentQueryResult;
};
export type AiAgentStreamHandlers = {
onStepStart?: (step: AiAgentStepExt) => void;
onStep?: (step: AiAgentStepExt) => void;
onDelta?: (delta: string) => void;
onDone?: (res: AiAgentRes) => void;
onError?: (error: string, partial?: Partial<AiAgentRes>) => void;
};
/** Agent SSE(stream=true);不可用时回落非流式。signal 供「停止」。 */
export async function callAiAgentStream(
payload: {
prompt: string;
connectionId?: string;
driver?: string;
database?: string;
model?: string;
},
handlers: AiAgentStreamHandlers,
signal?: AbortSignal,
): Promise<AiAgentRes> {
try {
const res = await fetch(`${API}/api/ai/agent`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "text/event-stream",
...authHeaders(),
},
body: JSON.stringify({ ...payload, stream: true }),
signal,
});
if (!res.ok || !res.body || !(res.headers.get("content-type") || "").includes("text/event-stream")) {
const plain = await callAiAgent(payload);
handlers.onDone?.(plain);
return plain;
}
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "";
let content = "";
const steps: AiAgentStep[] = [];
let rounds = 0;
let finalRes: AiAgentRes | null = null;
const handleEvent = (event: string, data: string) => {
let ev: Record<string, unknown>;
try {
ev = JSON.parse(data) as Record<string, unknown>;
} catch {
return;
}
const typ = event || String(ev.type || "");
if (typ === "step_start" && ev.step) {
handlers.onStepStart?.(ev.step as AiAgentStepExt);
} else if (typ === "step" && ev.step) {
const st = ev.step as AiAgentStepExt;
steps.push(st);
handlers.onStep?.(st);
} else if (typ === "delta") {
const d = String(ev.delta || "");
content += d;
handlers.onDelta?.(d);
} else if (typ === "done") {
finalRes = {
content: String(ev.content ?? content),
sql: ev.sql ? String(ev.sql) : undefined,
steps: (ev.steps as AiAgentStep[]) || steps,
rounds: Number(ev.rounds ?? rounds) || 0,
};
rounds = finalRes.rounds;
handlers.onDone?.(finalRes);
} else if (typ === "error") {
handlers.onError?.(String(ev.error || "agent error"), { content, steps, rounds: Number(ev.rounds) || 0 });
}
};
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
let idx: number;
while ((idx = buf.indexOf("\n\n")) >= 0) {
const chunk = buf.slice(0, idx);
buf = buf.slice(idx + 2);
let event = "";
let data = "";
for (const line of chunk.split("\n")) {
if (line.startsWith("event:")) event = line.slice(6).trim();
else if (line.startsWith("data:")) data += (data ? "\n" : "") + line.slice(5).trim();
}
if (data) handleEvent(event, data);
}
}
if (finalRes) return finalRes;
const fallback: AiAgentRes = { content, steps, rounds };
handlers.onDone?.(fallback);
return fallback;
} catch (e) {
if (signal?.aborted) throw e;
const plain = await callAiAgent(payload);
handlers.onDone?.(plain);
return plain;
}
}
export type McpStatus = {
configured: boolean;
mode: string;
whitelistCount: number;
writesEnabled: boolean;
active: boolean;
recent?: { time: string; tool: string; ok: boolean; summary?: string; error?: string; source?: string }[];
};
export async function getMcpStatus(signal?: AbortSignal) {
return get<McpStatus>("/api/ai/mcp-status", signal);
}
export async function confirmAiAgentWrite(payload: {
connectionId: string;
sql?: string;
command?: string;
database?: string;
kind?: "query" | "redis";
}) {
return post<Record<string, unknown>>("/api/ai/agent/confirm", payload);
}
/** C4:AI 配置(key 脱敏,只回掩码与是否已配置);protocol/authStyle/allowInsecureHttp 见 aiPresets.ts */
export type AiConfig = {
provider: string;