feat(agent): support Responses steering and project-aware tools
Use the dedicated TJWATER_AGENT_URL and Responses stream protocol. Keep running turns active while users steer or queue follow-ups and edit pending delivery. Refresh the model catalog before generation and preserve approval mode selection. Integrate scoped investigation messaging/presentation, geographic place selection, current Agent composer styles and supporting workbench contracts. Validation: 77 focused Vitest checks, 21 browser cases, lint, typecheck and production build. Preserve direct Server/Agent routing and project authentication.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { InvestigationReportView } from "./investigation-report-view";
|
||||
import { formatDateTime as formatDisplayDateTime } from "@/shared/utils/date-time";
|
||||
import { InvestigationTaskReport } from "./investigation-task-report";
|
||||
import type { InvestigationDiscussion } from "../api/investigation-report";
|
||||
import type { OperationalTaskReportPage } from "../api/operational-task-client";
|
||||
@@ -61,6 +62,7 @@ export type ExecutionPlanView = {
|
||||
};
|
||||
|
||||
type AgentExecutionPlanPanelProps = {
|
||||
onLocate?: (targets: import("../api/investigation-report").InvestigationMapTarget[]) => void;
|
||||
task: OperationalTask;
|
||||
onContinue: (discussion?: InvestigationDiscussion) => void;
|
||||
onDecision: (actionId: string, expectedVersion: number, decision: OperationalTaskDecision, comment?: string) => Promise<unknown>;
|
||||
@@ -77,6 +79,7 @@ type AgentExecutionPlanPanelProps = {
|
||||
};
|
||||
|
||||
export function AgentExecutionPlanPanel({
|
||||
onLocate,
|
||||
task,
|
||||
onContinue,
|
||||
onDecision,
|
||||
@@ -92,7 +95,7 @@ export function AgentExecutionPlanPanel({
|
||||
focusActionId
|
||||
}: AgentExecutionPlanPanelProps) {
|
||||
if (task.task_type === "event_investigation") return <InvestigationTaskReport key={task.task_id} task={task}
|
||||
onContinue={onContinue} onLoadReports={onLoadReports} onReportViewed={onReportViewed} focusActionId={focusActionId}
|
||||
onContinue={onContinue} onLoadReports={onLoadReports} onReportViewed={onReportViewed} focusActionId={focusActionId} onLocate={onLocate}
|
||||
renderAction={(action) => <HumanActionCard key={action.action_id} task={task} action={action} onDecision={onDecision}
|
||||
onExecute={onExecute} onDraftUpdate={onDraftUpdate} onConfirmDispatch={onConfirmDispatch} onReconcile={onReconcile} onRefresh={onRefresh} />}
|
||||
renderEvidence={(item) => <EvidenceDisclosure key={item.evidence_id} item={item} onLoadSnapshot={onLoadEvidenceSnapshot} />}
|
||||
@@ -412,7 +415,7 @@ function EvidenceDisclosure({ item, onLoadSnapshot }: { item: OperationalTaskEvi
|
||||
catch (nextError) { setError(nextError instanceof Error ? nextError.message : "数据快照读取失败。"); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
return <Disclosure label={<span className="line-clamp-2 text-left text-xs leading-5 text-slate-700">{item.summary}</span>} className="surface-control rounded-lg border border-[var(--surface-divider)]/70 px-2.5">
|
||||
return <Disclosure label={<span className="line-clamp-2 wrap-anywhere text-left text-xs leading-5 text-slate-700">{item.summary}</span>} className="surface-control rounded-lg border border-[var(--surface-divider)]/70 px-2.5">
|
||||
<div className="border-t border-[var(--surface-divider)]/70 py-2">
|
||||
<dl className="grid gap-1 text-[11px] leading-5"><EvidenceMeta label="来源" value={item.source_label} /><EvidenceMeta label="时间" value={formatDateTime(item.observed_at)} /><EvidenceMeta label="类型" value={evidenceKindLabel(item.kind)} />{item.source_ref ? <EvidenceMeta label="引用" value={item.source_ref} mono /> : null}</dl>
|
||||
{item.has_snapshot && !snapshot ? <Button className="mt-2" variant="outline" size="sm" disabled={loading} onClick={() => void load()}>{loading ? <Loader2 className="animate-spin" /> : null}查看数据快照</Button> : null}
|
||||
@@ -424,7 +427,7 @@ function EvidenceDisclosure({ item, onLoadSnapshot }: { item: OperationalTaskEvi
|
||||
|
||||
function SnapshotSummary({ snapshot }: { snapshot: OperationalTaskDataSnapshot }) {
|
||||
const rows = snapshotRows(snapshot.data).slice(0, 12);
|
||||
return <section aria-label="数据快照" className="mt-2 rounded-lg bg-[var(--surface-reading)] px-2.5 py-2 shadow-[inset_0_0_0_1px_rgba(148,163,184,0.3)]"><p className="text-[11px] font-semibold text-slate-700">{snapshot.source_ref} · 查询于 {formatDateTime(snapshot.queried_at)}</p><dl className="mt-1 grid gap-1 text-[11px] leading-5">{rows.map(([key, value]) => <EvidenceMeta key={key} label={key} value={value} />)}</dl>{snapshot.truncated ? <p className="mt-1 text-[11px] text-[var(--status-warning-foreground)]">数据已按有界查询范围截断。</p> : null}</section>;
|
||||
return <section aria-label="数据快照" className="mt-2 min-w-0 rounded-lg bg-[var(--surface-reading)] px-2.5 py-2 shadow-[inset_0_0_0_1px_rgba(148,163,184,0.3)]"><p className="wrap-anywhere text-[11px] font-semibold text-slate-700">{snapshot.source_ref} · 查询于 {formatDateTime(snapshot.queried_at)}</p><dl className="mt-1 grid gap-1 text-[11px] leading-5">{rows.map(([key, value]) => <EvidenceMeta key={key} label={key} value={value} />)}</dl>{snapshot.truncated ? <p className="mt-1 text-[11px] text-[var(--status-warning-foreground)]">数据已按有界查询范围截断。</p> : null}</section>;
|
||||
}
|
||||
|
||||
function snapshotRows(value: unknown, prefix = ""): [string, string][] {
|
||||
@@ -434,7 +437,7 @@ function snapshotRows(value: unknown, prefix = ""): [string, string][] {
|
||||
}
|
||||
|
||||
function EvidenceMeta({ label, value, mono = false }: { label: string; value: string; mono?: boolean }) {
|
||||
return <div className="grid grid-cols-[48px_1fr] gap-2"><dt className="font-semibold text-[var(--ink-muted)]">{label}</dt><dd className={cn("break-words text-slate-700", mono && "font-mono")}>{value}</dd></div>;
|
||||
return <div className="grid min-w-0 grid-cols-[48px_minmax(0,1fr)] gap-2"><dt className="min-w-0 wrap-anywhere font-semibold text-[var(--ink-muted)]">{label}</dt><dd className={cn("min-w-0 wrap-anywhere text-slate-700", mono && "font-mono")}>{value}</dd></div>;
|
||||
}
|
||||
|
||||
export function AccessRequestCard({ request, nodeTitle, canDecide, onDecision, onRefresh }: {
|
||||
@@ -662,7 +665,7 @@ function dataScopeLabel(scope: string) { return ({
|
||||
} as Record<string, string>)[scope] ?? scope; }
|
||||
function taskStatusLabel(status: OperationalTask["status"]) { return { queued: "排队中", running: "处理中", waiting_human: "待人工", completed: "已完成", failed: "失败", cancelled: "已取消" }[status]; }
|
||||
function evidenceKindLabel(kind: OperationalTaskEvidence["kind"]) { return { system_data: "系统数据", external_feedback: "外部反馈", human_input: "人工提供", synthetic: "合成演练" }[kind]; }
|
||||
function formatDateTime(value: string) { const parsed = new Date(value); return Number.isNaN(parsed.getTime()) ? value : new Intl.DateTimeFormat("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false }).format(parsed); }
|
||||
function formatDateTime(value: string) { return formatDisplayDateTime(value, { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }); }
|
||||
function toLocalDateTime(value: unknown) {
|
||||
if (typeof value !== "string") return "";
|
||||
const date = new Date(value);
|
||||
|
||||
Reference in New Issue
Block a user