feat(goal-run): add multi-checkpoint goal orchestration with H5 and admin surfaces.
Persist goal runs in MySQL, bind agent runs to checkpoints, expose awaiting-approval UX in chat, and add admin inspection routes with local verify scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useChat } from '../context/ChatProvider';
|
||||
import { INSUFFICIENT_BALANCE_NOTICE } from '../hooks/useTKMindChat';
|
||||
import { useGoalRunBanner } from '../hooks/useGoalRunBanner';
|
||||
import type { CapabilityMap, PortalUser, SessionSummary } from '../types';
|
||||
import { useNetworkStatus } from '../hooks/useNetworkStatus';
|
||||
import { getSessionDisplayName } from '../utils/sessions';
|
||||
@@ -15,6 +16,7 @@ import { WechatBindPrompt } from './WechatBindPrompt';
|
||||
import { WechatAccountButton } from './WechatAccountButton';
|
||||
import { ChatHeaderMoreMenu } from './ChatHeaderMoreMenu';
|
||||
import { NotificationCenter } from './NotificationCenter';
|
||||
import { GoalRunAwaitingBanner } from './GoalRunAwaitingBanner';
|
||||
import type { MindSpaceSaveCategory } from '../types';
|
||||
|
||||
function RecentSessionRail({
|
||||
@@ -212,8 +214,18 @@ export function ChatView({
|
||||
openSubscribe,
|
||||
uploadChatImage,
|
||||
uploadChatAttachment,
|
||||
followAgentRun,
|
||||
goalRunEnabled,
|
||||
} = useChat();
|
||||
const online = useNetworkStatus();
|
||||
const goalRunBanner = useGoalRunBanner({
|
||||
userId: user?.id,
|
||||
sessionId: session?.id,
|
||||
chatState,
|
||||
submit,
|
||||
followAgentRun,
|
||||
featureEnabled: goalRunEnabled,
|
||||
});
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [packagePanelSession, setPackagePanelSession] = useState<SessionSummary | null>(null);
|
||||
const [armedSessionId, setArmedSessionId] = useState<string | null>(null);
|
||||
@@ -487,6 +499,16 @@ export function ChatView({
|
||||
|
||||
{user && <WechatBindPrompt returnTo={window.location.pathname} />}
|
||||
|
||||
{goalRunBanner.enabled && (
|
||||
<GoalRunAwaitingBanner
|
||||
items={goalRunBanner.items}
|
||||
busy={goalRunBanner.bannerBusy}
|
||||
onContinue={goalRunBanner.handleContinue}
|
||||
onDismiss={goalRunBanner.dismissItem}
|
||||
onRefresh={goalRunBanner.refresh}
|
||||
/>
|
||||
)}
|
||||
|
||||
{typeof balanceCents === 'number' && balanceCents <= 0 && (
|
||||
<div className="banner banner-error">
|
||||
<span>余额不足,请充值后继续使用</span>
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { useState } from 'react';
|
||||
import { cancelGoalRun, type GoalRunAwaitingItem } from '../api/goalRun';
|
||||
|
||||
type GoalRunAwaitingBannerProps = {
|
||||
items: GoalRunAwaitingItem[];
|
||||
busy: boolean;
|
||||
onContinue: (input: {
|
||||
goalRunId: string;
|
||||
checkpointId: string;
|
||||
feedback?: string | null;
|
||||
}) => Promise<void>;
|
||||
onDismiss: (goalRunId: string) => void;
|
||||
onRefresh: () => Promise<void>;
|
||||
};
|
||||
|
||||
function GoalRunAwaitingCard({
|
||||
item,
|
||||
busy,
|
||||
onContinue,
|
||||
onDismiss,
|
||||
onRefresh,
|
||||
}: {
|
||||
item: GoalRunAwaitingItem;
|
||||
busy: boolean;
|
||||
onContinue: GoalRunAwaitingBannerProps['onContinue'];
|
||||
onDismiss: (goalRunId: string) => void;
|
||||
onRefresh: () => Promise<void>;
|
||||
}) {
|
||||
const [feedback, setFeedback] = useState('');
|
||||
const [action, setAction] = useState<'continue' | 'cancel' | null>(null);
|
||||
const [localError, setLocalError] = useState<string | null>(null);
|
||||
const { goal, checkpoint } = item;
|
||||
const summary = checkpoint.outputSummary?.trim();
|
||||
|
||||
const handleContinue = async () => {
|
||||
setLocalError(null);
|
||||
setAction('continue');
|
||||
try {
|
||||
await onContinue({
|
||||
goalRunId: goal.id,
|
||||
checkpointId: checkpoint.id,
|
||||
feedback: feedback.trim() || null,
|
||||
});
|
||||
onDismiss(goal.id);
|
||||
} catch (err) {
|
||||
setLocalError(err instanceof Error ? err.message : String(err));
|
||||
await onRefresh();
|
||||
} finally {
|
||||
setAction(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = async () => {
|
||||
setLocalError(null);
|
||||
setAction('cancel');
|
||||
try {
|
||||
await cancelGoalRun(goal.id);
|
||||
onDismiss(goal.id);
|
||||
} catch (err) {
|
||||
setLocalError(err instanceof Error ? err.message : String(err));
|
||||
await onRefresh();
|
||||
} finally {
|
||||
setAction(null);
|
||||
}
|
||||
};
|
||||
|
||||
const cardBusy = busy || action != null;
|
||||
|
||||
return (
|
||||
<div className="banner banner-info goal-run-awaiting-banner">
|
||||
<div className="goal-run-awaiting-banner-body">
|
||||
<strong>{goal.title}</strong>
|
||||
<span>
|
||||
阶段「{checkpoint.title}」已完成,请确认后继续下一阶段。
|
||||
</span>
|
||||
{summary ? <span className="goal-run-awaiting-summary">{summary}</span> : null}
|
||||
<textarea
|
||||
className="goal-run-awaiting-feedback"
|
||||
rows={2}
|
||||
placeholder="可选:补充反馈或修改方向"
|
||||
value={feedback}
|
||||
disabled={cardBusy}
|
||||
onChange={(event) => setFeedback(event.target.value)}
|
||||
/>
|
||||
{localError ? <span className="goal-run-awaiting-error">{localError}</span> : null}
|
||||
</div>
|
||||
<div className="goal-run-awaiting-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="banner-action"
|
||||
disabled={cardBusy}
|
||||
onClick={() => void handleContinue()}
|
||||
>
|
||||
{action === 'continue' ? '处理中…' : '确认并继续'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="banner-dismiss"
|
||||
disabled={cardBusy}
|
||||
onClick={() => void handleCancel()}
|
||||
>
|
||||
{action === 'cancel' ? '取消中…' : '取消目标'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function GoalRunAwaitingBanner({
|
||||
items,
|
||||
busy,
|
||||
onContinue,
|
||||
onDismiss,
|
||||
onRefresh,
|
||||
}: GoalRunAwaitingBannerProps) {
|
||||
if (!items.length) return null;
|
||||
|
||||
return (
|
||||
<div className="goal-run-awaiting-stack">
|
||||
{items.map((item) => (
|
||||
<GoalRunAwaitingCard
|
||||
key={item.goal.id}
|
||||
item={item}
|
||||
busy={busy}
|
||||
onContinue={onContinue}
|
||||
onDismiss={onDismiss}
|
||||
onRefresh={onRefresh}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user