feat: expand intelligent investigation workspace
Generic Container CI/CD / test-build-publish (push) Successful in 1m52s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 1m52s

This commit is contained in:
2026-10-30 17:30:19 +08:00
parent ddd03dbff6
commit c0e4231b83
223 changed files with 13155 additions and 5304 deletions
+42 -3
View File
@@ -2,6 +2,7 @@ import { createInteractionRequestApi, type InteractionRequestApi } from "./inter
import { env } from "@/shared/config/env";
import type { AccessTokenProvider } from "@/shared/auth/keycloak-auth";
import { agentMessagePageSchema, type AgentMessagePage } from "./agent-messages";
import { AnswerViewStateApiError, readAnswerViewStateResponse, type AnswerViewStateRecord, type AnswerViewStateWrite } from "./answer-view-state";
export type AgentRunStatus = "running" | "completed" | "error" | "aborted" | "waiting_input";
export type AgentSessionPurpose = "ordinary_chat" | "task_execution" | "task_collaboration";
@@ -20,8 +21,8 @@ export type AgentEventTriggerContext = {
summary: string;
location?: string | null;
priority: "low" | "normal" | "high" | "urgent";
scenario_code: "WQ-SECONDARY-SUPPLY-001";
origin: "synthetic";
scenario_code?: string | null;
origin: "system_data";
};
};
@@ -156,6 +157,8 @@ export type AgentSessionStreamEvent =
export type AgentApiClient = InteractionRequestApi & {
getHealth: (signal: AbortSignal) => Promise<void>;
getAnswerViewState: (sessionId: string, messageId: string, blockId: string) => Promise<AnswerViewStateRecord>;
putAnswerViewState: (sessionId: string, state: AnswerViewStateWrite) => Promise<AnswerViewStateRecord>;
downloadFile: (path: string) => Promise<void>;
submitResponseInput: (sessionId: string, text: string, delivery: AgentInputDelivery, id: string) => Promise<AgentResponseInput>;
listResponseInputs: (sessionId: string) => Promise<AgentResponseInput[]>;
@@ -249,14 +252,41 @@ export function createAgentApiClient(
return data as T;
};
let answerStateActor: string | undefined;
const checkedAnswerStateToken = options.getAccessToken ? async () => {
const token = await options.getAccessToken!();
const actor = answerStateTokenActor(token);
if (answerStateActor !== undefined && actor !== answerStateActor) {
throw new AnswerViewStateApiError("登录身份已变化,请刷新后继续。", 403, "ANSWER_VIEW_IDENTITY_CHANGED");
}
answerStateActor = actor;
return token;
} : undefined;
const answerStateRequest = async (path: string, init?: RequestInit) => {
const response = await fetchWithFallback(candidates, activeBaseUrl, setActiveBaseUrl, path, init,
checkedAnswerStateToken, options.getProjectId);
// An identity change while awaiting the network also invalidates the result.
await checkedAnswerStateToken?.();
return response;
};
return {
...createInteractionRequestApi(request),
...createInteractionRequestApi(answerStateRequest),
async getHealth(signal) {
const response = await fetchWithFallback(candidates, activeBaseUrl, setActiveBaseUrl, "/health", { signal, cache: "no-store" }, undefined, undefined, "");
if (!response.ok) throw new Error("Agent 服务暂不可用");
const data: unknown = await response.json();
if (!isRecord(data) || data.ok !== true || data.ready !== true) throw new Error("Agent 服务尚未就绪");
},
async getAnswerViewState(sessionId, messageId, blockId) {
const query = new URLSearchParams({ message_id: messageId, block_id: blockId });
return readAnswerViewStateResponse(await answerStateRequest(`/session/${encodeURIComponent(sessionId)}/ui-state?${query}`));
},
async putAnswerViewState(sessionId, state) {
return readAnswerViewStateResponse(await answerStateRequest(`/session/${encodeURIComponent(sessionId)}/ui-state`, {
method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(state)
}));
},
async downloadFile(path) {
const match = /^\/api\/v1(\/session\/[^/?#]+\/files\/mcp-[a-f0-9-]{36}\.(?:zip|xlsx))$/.exec(path);
if (!match) throw new Error("无效的会话文件地址");
@@ -845,3 +875,12 @@ function readMessagePagination(value: unknown): AgentMessagePagination | undefin
if (!isRecord(value)) return undefined;
return { hasMore: value.has_more === true, beforeCursor: typeof value.before_cursor === "string" ? value.before_cursor : null };
}
function answerStateTokenActor(token: string | null): string {
if (!token) return "development-context";
try {
const payload: unknown = JSON.parse(atob(token.split(".")[1].replaceAll("-", "+").replaceAll("_", "/")));
if (isRecord(payload) && typeof payload.sub === "string") return JSON.stringify([payload.iss, payload.sub]);
} catch { /* Non-JWT development credentials use the enclosing workbench owner. */ }
return "development-context";
}