feat(ui,docs): Agent 流式侧栏 + 结果区/MCP 点/确认条 + 官网文案
- 侧栏消费 Agent SSE:时间线边 append、正文 ▌、停止按钮 - 「在结果区打开」不抢编辑器 focus;MCP 活跃点;写操作确认条 - 文档/落地页补充 Ask|Agent 与 npx @rlwj-lab/opsxdb-mcp(不宣传默认可写)
This commit is contained in:
1 parent
2336c9433c
commit
979efb606d
9 files changed
+580
-92
No files matched your search
+143
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user