feat: add project-aware agent workspace
Generic Container CI/CD / test-build-publish (push) Successful in 2m9s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 2m39s

This commit is contained in:
2026-10-22 18:52:18 +08:00
parent c6a0e74e72
commit 2d8506fea0
191 changed files with 12373 additions and 3556 deletions
@@ -3,6 +3,8 @@ import type { AgentApiClient, AgentChatSessionSummary, AgentPromptSuggestion, Ag
import type { WorkbenchAlert, ScheduledConditionItem, WorkbenchAlertDataSource, WorkbenchRuntimeDataSource } from "../types";
import type { AnalysisArtifact } from "../workspace/workspace-model";
import { useWorkbenchSessionResources } from "./workbench-session-resources";
type ActivityInputs = {
alerts: WorkbenchAlert[];
alertDataSource: WorkbenchAlertDataSource;
@@ -44,38 +46,52 @@ 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, sessionId, request, alerts, alertDataSource, conditions, runtimeDataSource, artifact, sessions }: ActivityInputs & {
export function useAgentPromptSuggestions({ enabled, model, cacheScope = "project", request, alerts, alertDataSource, conditions, runtimeDataSource, artifact, sessions }: ActivityInputs & {
enabled: boolean;
model: string;
sessionId: string | null;
cacheScope?: string;
request: AgentApiClient["generatePromptSuggestions"];
}) {
const { cache } = useWorkbenchSessionResources();
const activities = useMemo(() => collectRecentAgentActivities({ alerts, alertDataSource, conditions, runtimeDataSource, artifact, sessions }),
[alerts, alertDataSource, conditions, runtimeDataSource, artifact, sessions]);
const snapshot = useRef(activities);
useEffect(() => { snapshot.current = activities; }, [activities]);
const key = JSON.stringify([enabled, model, sessionId]);
const [state, setState] = useState<{ key: string; suggestions: DisplaySuggestion[]; loading: boolean; unavailable: boolean }>({
key: "", suggestions: [], loading: false, unavailable: false,
// Heartbeat timestamps change while the activity itself stays the same.
const digest = JSON.stringify([...activities].sort((a, b) => a.id.localeCompare(b.id)).map((activity) => ({
id: activity.id, kind: activity.kind, title: activity.title, summary: activity.summary,
status: activity.status, origin: activity.origin,
})));
const activitySnapshot = useRef(activities);
activitySnapshot.current = activities;
const key = JSON.stringify(["suggestions", cacheScope, model, digest]);
const [state, setState] = useState<{ scope: string; suggestions: DisplaySuggestion[]; loading: boolean; unavailable: boolean }>({
scope: cacheScope, suggestions: [], loading: false, unavailable: false,
});
useEffect(() => {
if (!enabled) return;
const controller = new AbortController();
const context = snapshot.current;
setState({ key, suggestions: [], loading: true, unavailable: false });
void request(context, model, controller.signal).then((suggestions) => {
if (controller.signal.aborted) return;
let disposed = false;
const previous = cache.peek<DisplaySuggestion[]>(key);
const apply = (suggestions: DisplaySuggestion[]) => {
if (!disposed) setState({ scope: cacheScope, suggestions, loading: false, unavailable: false });
};
if (previous?.value) apply(previous.value);
else setState((current) => ({ scope: cacheScope,
suggestions: current.scope === cacheScope ? current.suggestions : [], loading: true, unavailable: false }));
void cache.load(key, async () => {
const context = activitySnapshot.current;
const suggestions = await request(context, model, AbortSignal.timeout(60_000));
const sourceById = new Map(context.map((item) => [item.id, item]));
setState({ key, loading: false, unavailable: false, suggestions: suggestions.map((item) => {
return suggestions.map((item) => {
const source = item.activityId ? sourceById.get(item.activityId) : undefined;
return { ...item, sourceLabel: source
? `${source.origin === "demo" ? "演示" : ""}${activityLabels[source.kind]} · ${timeFormatter.format(new Date(source.occurred_at))}`
: "供水任务建议" };
}) });
}).catch(() => {
if (!controller.signal.aborted) setState({ key, suggestions: [], loading: false, unavailable: true });
});
}, 10 * 60_000, { errorTtl: 30_000 }).then(apply).catch(() => {
if (!disposed) setState((current) => ({ scope: cacheScope,
suggestions: current.scope === cacheScope ? current.suggestions : [], loading: false, unavailable: true }));
});
return () => controller.abort();
}, [enabled, key, model, request]);
return state.key === key ? state : { suggestions: [], loading: enabled, unavailable: false };
return () => { disposed = true; };
}, [cache, cacheScope, digest, enabled, key, model, request]);
return state.scope === cacheScope ? state : { suggestions: [], loading: enabled, unavailable: false };
}