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
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { AgentApiClient, AgentChatSessionSummary, AgentPromptSuggestion, AgentRecentActivity } from "@/features/agent";
import type { WorkbenchAlert, ScheduledConditionItem, WorkbenchAlertDataSource, WorkbenchRuntimeDataSource } from "../types";
import type { AnalysisArtifact } from "../workspace/workspace-model";
@@ -46,11 +46,12 @@ export function collectRecentAgentActivities(input: ActivityInputs, now = Date.n
return [...new Map([...alerts, ...tasks, ...analyses, ...sessions].map((item) => [item.id, item])).values()];
}
export function useAgentPromptSuggestions({ enabled, model, cacheScope = "project", request, alerts, alertDataSource, conditions, runtimeDataSource, artifact, sessions }: ActivityInputs & {
export function useAgentPromptSuggestions({ enabled, model, cacheScope = "project", recoveryVersion = 0, request, alerts, alertDataSource, conditions, runtimeDataSource, artifact, sessions }: ActivityInputs & {
enabled: boolean;
model: string;
sessionId: string | null;
cacheScope?: string;
recoveryVersion?: number;
request: AgentApiClient["generatePromptSuggestions"];
}) {
const { cache } = useWorkbenchSessionResources();
@@ -64,13 +65,22 @@ export function useAgentPromptSuggestions({ enabled, model, cacheScope = "projec
const activitySnapshot = useRef(activities);
activitySnapshot.current = activities;
const key = JSON.stringify(["suggestions", cacheScope, model, digest]);
const [retryVersion, setRetryVersion] = useState(0);
const [refreshVersion, setRefreshVersion] = useState(0);
const attempts = useRef({ key, count: 0, recoveryVersion, retryVersion });
const retry = useCallback(() => setRetryVersion((version) => version + 1), []);
const [state, setState] = useState<{ scope: string; suggestions: DisplaySuggestion[]; loading: boolean; unavailable: boolean }>({
scope: cacheScope, suggestions: [], loading: false, unavailable: false,
});
useEffect(() => {
if (!enabled) return;
if (!enabled || !model.trim()) return;
let disposed = false;
let timer: ReturnType<typeof setTimeout> | undefined;
const previous = cache.peek<DisplaySuggestion[]>(key);
const recovered = attempts.current.recoveryVersion !== recoveryVersion || attempts.current.retryVersion !== retryVersion;
if (attempts.current.key !== key || recovered) attempts.current = { key, count: 0, recoveryVersion, retryVersion };
const attempt = attempts.current;
const context = activitySnapshot.current;
const apply = (suggestions: DisplaySuggestion[]) => {
if (!disposed) setState({ scope: cacheScope, suggestions, loading: false, unavailable: false });
};
@@ -78,7 +88,7 @@ export function useAgentPromptSuggestions({ enabled, model, cacheScope = "projec
else setState((current) => ({ scope: cacheScope,
suggestions: current.scope === cacheScope ? current.suggestions : [], loading: true, unavailable: false }));
void cache.load(key, async () => {
const context = activitySnapshot.current;
attempt.count++;
const suggestions = await request(context, model, AbortSignal.timeout(60_000));
const sourceById = new Map(context.map((item) => [item.id, item]));
return suggestions.map((item) => {
@@ -87,11 +97,17 @@ export function useAgentPromptSuggestions({ enabled, model, cacheScope = "projec
? `${source.origin === "demo" ? "演示" : ""}${activityLabels[source.kind]} · ${timeFormatter.format(new Date(source.occurred_at))}`
: "供水任务建议" };
});
}, 10 * 60_000, { errorTtl: 30_000 }).then(apply).catch(() => {
if (!disposed) setState((current) => ({ scope: cacheScope,
}, 10 * 60_000, { errorTtl: 30_000, force: recovered && !!previous?.error }).then(apply).catch(() => {
if (disposed) return;
setState((current) => ({ scope: cacheScope,
suggestions: current.scope === cacheScope ? current.suggestions : [], loading: false, unavailable: true }));
// Cache expiry alone does not rerender the panel. Retry transient failures
// here, with a bounded budget; reopening still reuses the shared cache.
if (attempt.count < 3) {
timer = setTimeout(() => setRefreshVersion((version) => version + 1), Math.max(1, (cache.peek(key)?.expiresAt ?? Date.now() + 30_000) - Date.now()));
}
});
return () => { disposed = true; };
}, [cache, cacheScope, digest, enabled, key, model, request]);
return state.scope === cacheScope ? state : { suggestions: [], loading: enabled, unavailable: false };
return () => { disposed = true; clearTimeout(timer); };
}, [cache, cacheScope, digest, enabled, key, model, recoveryVersion, refreshVersion, request, retryVersion]);
return { ...(state.scope === cacheScope ? state : { suggestions: [], loading: enabled, unavailable: false }), retry };
}