吸收数字员工PlanStep阻塞处理入口
This commit is contained in:
@@ -2248,6 +2248,41 @@
|
|||||||
color: #b42318;
|
color: #b42318;
|
||||||
border-color: rgba(180,35,24,0.22);
|
border-color: rgba(180,35,24,0.22);
|
||||||
}
|
}
|
||||||
|
.de-blocked-step-control-strip {
|
||||||
|
border-top: 1px solid rgba(180,35,24,0.12);
|
||||||
|
padding-top: 6px;
|
||||||
|
}
|
||||||
|
.de-blocked-step-control {
|
||||||
|
border-color: rgba(180,35,24,0.18);
|
||||||
|
background: rgba(255,248,247,0.92);
|
||||||
|
}
|
||||||
|
.de-blocked-step-control .de-task-agent-control-actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.de-blocked-input-modal { max-width: 520px; }
|
||||||
|
.de-blocked-input-field {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
color: #17344f;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.de-blocked-input-field textarea {
|
||||||
|
min-height: 120px;
|
||||||
|
resize: vertical;
|
||||||
|
border: 1px solid rgba(79,172,254,0.2);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
color: #12344f;
|
||||||
|
background: rgba(255,255,255,0.82);
|
||||||
|
}
|
||||||
|
.de-blocked-input-field textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: rgba(79,172,254,0.55);
|
||||||
|
box-shadow: 0 0 0 3px rgba(79,172,254,0.14);
|
||||||
|
}
|
||||||
.de-run-detail-list-scroll {
|
.de-run-detail-list-scroll {
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import type {
|
|||||||
ChannelDetail,
|
ChannelDetail,
|
||||||
BrowserSession,
|
BrowserSession,
|
||||||
ApprovalSubscriptionPreferences,
|
ApprovalSubscriptionPreferences,
|
||||||
|
BusinessViewResponse,
|
||||||
|
PlanStepBusinessRow,
|
||||||
PlanStepDispatchPolicy,
|
PlanStepDispatchPolicy,
|
||||||
NotificationPreferences,
|
NotificationPreferences,
|
||||||
UserNotification,
|
UserNotification,
|
||||||
@@ -609,22 +611,22 @@ export function patchPlanStepDispatchPolicy(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPlanSteps(params: Record<string, string | number | null | undefined> = {}): Promise<any> {
|
export function getPlanSteps(params: Record<string, string | number | null | undefined> = {}): Promise<BusinessViewResponse<PlanStepBusinessRow>> {
|
||||||
const query = new URLSearchParams();
|
const query = new URLSearchParams();
|
||||||
Object.entries(params).forEach(([key, value]) => {
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
if (value !== undefined && value !== null && `${value}`.trim() !== '') query.set(key, `${value}`);
|
if (value !== undefined && value !== null && `${value}`.trim() !== '') query.set(key, `${value}`);
|
||||||
});
|
});
|
||||||
const suffix = query.toString();
|
const suffix = query.toString();
|
||||||
return apiFetch(`/api/digital-employee/plan-steps${suffix ? `?${suffix}` : ''}`);
|
return apiFetch<BusinessViewResponse<PlanStepBusinessRow>>(`/api/digital-employee/plan-steps${suffix ? `?${suffix}` : ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPlanStepGraph(params: Record<string, string | number | null | undefined> = {}): Promise<any> {
|
export function getPlanStepGraph(params: Record<string, string | number | null | undefined> = {}): Promise<BusinessViewResponse<PlanStepBusinessRow>> {
|
||||||
const query = new URLSearchParams();
|
const query = new URLSearchParams();
|
||||||
Object.entries(params).forEach(([key, value]) => {
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
if (value !== undefined && value !== null && `${value}`.trim() !== '') query.set(key, `${value}`);
|
if (value !== undefined && value !== null && `${value}`.trim() !== '') query.set(key, `${value}`);
|
||||||
});
|
});
|
||||||
const suffix = query.toString();
|
const suffix = query.toString();
|
||||||
return apiFetch(`/api/digital-employee/plan-step-graph${suffix ? `?${suffix}` : ''}`);
|
return apiFetch<BusinessViewResponse<PlanStepBusinessRow>>(`/api/digital-employee/plan-step-graph${suffix ? `?${suffix}` : ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function markNotificationRead(id: string): Promise<boolean> {
|
export function markNotificationRead(id: string): Promise<boolean> {
|
||||||
|
|||||||
@@ -2593,11 +2593,13 @@ function planStepRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
|
|||||||
dependency_status: stringValue(dependencyGraph.status) || stepDependencyStatus(step, blockedDependencies, waitingDependencies),
|
dependency_status: stringValue(dependencyGraph.status) || stepDependencyStatus(step, blockedDependencies, waitingDependencies),
|
||||||
blocked_dependencies: blockedDependencies,
|
blocked_dependencies: blockedDependencies,
|
||||||
blocked_dependency_count: blockedDependencies.length,
|
blocked_dependency_count: blockedDependencies.length,
|
||||||
|
blocked_dependency_details: planStepDependencyDetails(blockedDependencies, snapshot),
|
||||||
waiting_dependencies: waitingDependencies,
|
waiting_dependencies: waitingDependencies,
|
||||||
waiting_dependency_count: waitingDependencies.length,
|
waiting_dependency_count: waitingDependencies.length,
|
||||||
satisfied_dependencies: satisfiedDependencies,
|
satisfied_dependencies: satisfiedDependencies,
|
||||||
preferred_skill_ids: step.preferredSkillIds,
|
preferred_skill_ids: step.preferredSkillIds,
|
||||||
task_id: readiness.taskId,
|
task_id: readiness.taskId,
|
||||||
|
latest_run_id: readiness.latestRunId,
|
||||||
dispatchable: readiness.dispatchable,
|
dispatchable: readiness.dispatchable,
|
||||||
skip_reason: readiness.skipReason,
|
skip_reason: readiness.skipReason,
|
||||||
last_dispatch_status: stringValue(lastDispatch.status) || null,
|
last_dispatch_status: stringValue(lastDispatch.status) || null,
|
||||||
@@ -2626,8 +2628,11 @@ function planStepGraphRows(snapshot: QimingclawSnapshot | null): BusinessViewRow
|
|||||||
depends_on: arrayValue(row.depends_on),
|
depends_on: arrayValue(row.depends_on),
|
||||||
dependency_status: stringValue(row.dependency_status) || null,
|
dependency_status: stringValue(row.dependency_status) || null,
|
||||||
blocked_dependencies: arrayValue(row.blocked_dependencies),
|
blocked_dependencies: arrayValue(row.blocked_dependencies),
|
||||||
|
blocked_dependency_details: arrayValue(row.blocked_dependency_details),
|
||||||
waiting_dependencies: arrayValue(row.waiting_dependencies),
|
waiting_dependencies: arrayValue(row.waiting_dependencies),
|
||||||
satisfied_dependencies: arrayValue(row.satisfied_dependencies),
|
satisfied_dependencies: arrayValue(row.satisfied_dependencies),
|
||||||
|
task_id: stringValue(row.task_id) || null,
|
||||||
|
latest_run_id: stringValue(row.latest_run_id) || null,
|
||||||
dispatchable: row.dispatchable === true,
|
dispatchable: row.dispatchable === true,
|
||||||
skip_reason: stringValue(row.skip_reason) || null,
|
skip_reason: stringValue(row.skip_reason) || null,
|
||||||
sync_status: stringValue(row.sync_status) || null,
|
sync_status: stringValue(row.sync_status) || null,
|
||||||
@@ -2661,12 +2666,13 @@ function planStepDispatchReadiness(
|
|||||||
step: QimingclawPlanStepRecord,
|
step: QimingclawPlanStepRecord,
|
||||||
snapshot: QimingclawSnapshot | null,
|
snapshot: QimingclawSnapshot | null,
|
||||||
payload: Record<string, unknown>,
|
payload: Record<string, unknown>,
|
||||||
): { taskId: string | null; dispatchable: boolean; skipReason: string | null } {
|
): { taskId: string | null; latestRunId: string | null; dispatchable: boolean; skipReason: string | null } {
|
||||||
const tasks = runtimePlanTasks(snapshot, step.planId);
|
const tasks = runtimePlanTasks(snapshot, step.planId);
|
||||||
const task = tasks.find((candidate) => taskStepId(candidate) === step.id)
|
const task = tasks.find((candidate) => taskStepId(candidate) === step.id)
|
||||||
?? tasks.find((candidate) => !isTerminalRuntimeTaskStatus(candidate.status))
|
?? tasks.find((candidate) => !isTerminalRuntimeTaskStatus(candidate.status))
|
||||||
?? null;
|
?? null;
|
||||||
const taskId = task?.id ?? null;
|
const taskId = task?.id ?? null;
|
||||||
|
const latestRunId = task ? latestRunForTask(runtimeRuns(snapshot), task.id)?.id ?? null : null;
|
||||||
let skipReason: string | null = null;
|
let skipReason: string | null = null;
|
||||||
if (step.status.toLowerCase() !== 'ready') skipReason = 'not_ready';
|
if (step.status.toLowerCase() !== 'ready') skipReason = 'not_ready';
|
||||||
else if (!task) skipReason = 'missing_task';
|
else if (!task) skipReason = 'missing_task';
|
||||||
@@ -2674,7 +2680,39 @@ function planStepDispatchReadiness(
|
|||||||
else if (runtimeRuns(snapshot).some((run) => run.taskId === task.id && isActiveRuntimeRunStatus(run.status))) skipReason = 'task_already_running';
|
else if (runtimeRuns(snapshot).some((run) => run.taskId === task.id && isActiveRuntimeRunStatus(run.status))) skipReason = 'task_already_running';
|
||||||
else if (runtimeApprovals(snapshot).some((approval) => isOpenRuntimeApprovalStatus(approval.status) && (approval.planId === step.planId || approval.taskId === task.id || stringValue(asRecord(approval.payload)?.stepId ?? asRecord(approval.payload)?.step_id) === step.id))) skipReason = 'approval_pending';
|
else if (runtimeApprovals(snapshot).some((approval) => isOpenRuntimeApprovalStatus(approval.status) && (approval.planId === step.planId || approval.taskId === task.id || stringValue(asRecord(approval.payload)?.stepId ?? asRecord(approval.payload)?.step_id) === step.id))) skipReason = 'approval_pending';
|
||||||
else if (payload.auto_dispatch === false || payload.autoDispatch === false) skipReason = 'auto_dispatch_disabled';
|
else if (payload.auto_dispatch === false || payload.autoDispatch === false) skipReason = 'auto_dispatch_disabled';
|
||||||
return { taskId, dispatchable: !skipReason, skipReason };
|
return { taskId, latestRunId, dispatchable: !skipReason, skipReason };
|
||||||
|
}
|
||||||
|
|
||||||
|
function planStepDependencyDetails(dependencyIds: string[], snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
|
||||||
|
const stepsById = new Map(runtimePlanSteps(snapshot).map((step) => [step.id, step]));
|
||||||
|
return dependencyIds.map((dependencyId) => {
|
||||||
|
const dependency = stepsById.get(dependencyId);
|
||||||
|
if (!dependency) {
|
||||||
|
return {
|
||||||
|
step_id: dependencyId,
|
||||||
|
task_id: null,
|
||||||
|
latest_run_id: null,
|
||||||
|
title: dependencyId,
|
||||||
|
status: 'missing',
|
||||||
|
dependency_status: 'missing',
|
||||||
|
dispatchable: false,
|
||||||
|
skip_reason: 'missing_step',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const payload = asRecord(dependency.payload) ?? {};
|
||||||
|
const graph = asRecord(payload.dependencyGraph ?? payload.dependency_graph) ?? {};
|
||||||
|
const readiness = planStepDispatchReadiness(dependency, snapshot, payload);
|
||||||
|
return {
|
||||||
|
step_id: dependency.id,
|
||||||
|
task_id: readiness.taskId,
|
||||||
|
latest_run_id: readiness.latestRunId,
|
||||||
|
title: dependency.title,
|
||||||
|
status: dependency.status,
|
||||||
|
dependency_status: stringValue(graph.status) || dependency.status,
|
||||||
|
dispatchable: readiness.dispatchable,
|
||||||
|
skip_reason: readiness.skipReason,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTerminalRuntimeTaskStatus(status: string): boolean {
|
function isTerminalRuntimeTaskStatus(status: string): boolean {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { basePath } from '@/lib/basePath';
|
|||||||
import {
|
import {
|
||||||
dispatchPlanNow,
|
dispatchPlanNow,
|
||||||
getDigitalEmployeeWorkday,
|
getDigitalEmployeeWorkday,
|
||||||
|
getPlanStepGraph,
|
||||||
getSchedulerJobs,
|
getSchedulerJobs,
|
||||||
governanceCommand,
|
governanceCommand,
|
||||||
postDigitalEmployeeWorkdayAction,
|
postDigitalEmployeeWorkdayAction,
|
||||||
@@ -26,6 +27,8 @@ import type {
|
|||||||
DigitalEmployeeWorkEvent,
|
DigitalEmployeeWorkEvent,
|
||||||
DigitalEmployeeWorkdayProjection,
|
DigitalEmployeeWorkdayProjection,
|
||||||
GovernanceCommandKind,
|
GovernanceCommandKind,
|
||||||
|
PlanStepBusinessRow,
|
||||||
|
PlanStepDependencyDetail,
|
||||||
} from '@/types/api';
|
} from '@/types/api';
|
||||||
import type { DigitalNavigationTarget } from './navigation';
|
import type { DigitalNavigationTarget } from './navigation';
|
||||||
|
|
||||||
@@ -38,6 +41,7 @@ type EmployeeVisualState = 'skill' | 'browser' | 'call' | 'redial' | 'evidence'
|
|||||||
type EventStatusFilter = 'all' | 'running' | 'success' | 'danger';
|
type EventStatusFilter = 'all' | 'running' | 'success' | 'danger';
|
||||||
type TaskListFilter = 'all' | 'selected' | 'pending';
|
type TaskListFilter = 'all' | 'selected' | 'pending';
|
||||||
type TaskAgentControlCommand = Extract<GovernanceCommandKind, 'retry_task' | 'skip_task' | 'cancel_task'>;
|
type TaskAgentControlCommand = Extract<GovernanceCommandKind, 'retry_task' | 'skip_task' | 'cancel_task'>;
|
||||||
|
type BlockedPlanStepControlCommand = Extract<GovernanceCommandKind, 'retry_task' | 'skip_task' | 'cancel_task' | 'provide_task_input'>;
|
||||||
type ApprovalActionKind = 'comment' | 'delegate' | 'mark_timeout' | 'mark_risk' | 'clear_risk';
|
type ApprovalActionKind = 'comment' | 'delegate' | 'mark_timeout' | 'mark_risk' | 'clear_risk';
|
||||||
|
|
||||||
interface DigitalPlanItem {
|
interface DigitalPlanItem {
|
||||||
@@ -60,6 +64,11 @@ interface DigitalPlanItem {
|
|||||||
source: 'plan_template';
|
source: 'plan_template';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BlockedPlanStepInputTarget {
|
||||||
|
step: PlanStepBusinessRow;
|
||||||
|
dependency: PlanStepDependencyDetail | null;
|
||||||
|
}
|
||||||
|
|
||||||
interface ManagementSyncStatus {
|
interface ManagementSyncStatus {
|
||||||
running: boolean;
|
running: boolean;
|
||||||
syncing: boolean;
|
syncing: boolean;
|
||||||
@@ -796,6 +805,26 @@ function taskAgentControlLabel(command: TaskAgentControlCommand): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function blockedPlanStepControlLabel(command: BlockedPlanStepControlCommand): string {
|
||||||
|
switch (command) {
|
||||||
|
case 'retry_task': return '重试前置';
|
||||||
|
case 'skip_task': return '跳过前置';
|
||||||
|
case 'cancel_task': return '取消此步骤';
|
||||||
|
case 'provide_task_input': return '补充输入';
|
||||||
|
default: return command;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstActionableBlockedDependency(step: PlanStepBusinessRow): PlanStepDependencyDetail | null {
|
||||||
|
return (step.blocked_dependency_details ?? []).find((dependency) => Boolean(dependency.task_id)) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function blockedPlanStepSummary(step: PlanStepBusinessRow): string {
|
||||||
|
const dependency = firstActionableBlockedDependency(step) ?? step.blocked_dependency_details?.[0] ?? null;
|
||||||
|
const dependencyLabel = dependency ? `${dependency.title} ${dependency.status}` : `阻塞依赖 ${step.blocked_dependencies.length} 个`;
|
||||||
|
return `${step.title} ${step.dependency_status || step.status},${dependencyLabel}`;
|
||||||
|
}
|
||||||
|
|
||||||
function isDecisionOpen(status?: string): boolean {
|
function isDecisionOpen(status?: string): boolean {
|
||||||
return ['pending', 'requested'].includes(String(status || '').toLowerCase());
|
return ['pending', 'requested'].includes(String(status || '').toLowerCase());
|
||||||
}
|
}
|
||||||
@@ -916,6 +945,10 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
|||||||
const [profileSettingsOpen, setProfileSettingsOpen] = useState(false);
|
const [profileSettingsOpen, setProfileSettingsOpen] = useState(false);
|
||||||
const [reportPreviewOpen, setReportPreviewOpen] = useState(false);
|
const [reportPreviewOpen, setReportPreviewOpen] = useState(false);
|
||||||
const [selectedSyncFailure, setSelectedSyncFailure] = useState<ManagementSyncFailure | null>(null);
|
const [selectedSyncFailure, setSelectedSyncFailure] = useState<ManagementSyncFailure | null>(null);
|
||||||
|
const [blockedPlanSteps, setBlockedPlanSteps] = useState<PlanStepBusinessRow[]>([]);
|
||||||
|
const [blockedPlanStepError, setBlockedPlanStepError] = useState<string | null>(null);
|
||||||
|
const [blockedInputTarget, setBlockedInputTarget] = useState<BlockedPlanStepInputTarget | null>(null);
|
||||||
|
const [blockedInputDraft, setBlockedInputDraft] = useState('');
|
||||||
const [profileForm, setProfileForm] = useState({
|
const [profileForm, setProfileForm] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
company: '',
|
company: '',
|
||||||
@@ -986,7 +1019,19 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
|||||||
setManagementSyncStatus(status);
|
setManagementSyncStatus(status);
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.allSettled([projectionPromise, jobsPromise, syncStatusPromise]);
|
const planStepGraphPromise = getPlanStepGraph({ status: 'blocked', page_size: 6 })
|
||||||
|
.then((response) => {
|
||||||
|
if (!mountedRef.current) return;
|
||||||
|
setBlockedPlanSteps(response.items.filter((step) => step.status === 'blocked'));
|
||||||
|
setBlockedPlanStepError(null);
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
if (!mountedRef.current) return;
|
||||||
|
setBlockedPlanSteps([]);
|
||||||
|
setBlockedPlanStepError(error instanceof Error ? error.message : '阻塞步骤加载失败');
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.allSettled([projectionPromise, jobsPromise, syncStatusPromise, planStepGraphPromise]);
|
||||||
if (mountedRef.current) {
|
if (mountedRef.current) {
|
||||||
setProjectionLoading(false);
|
setProjectionLoading(false);
|
||||||
}
|
}
|
||||||
@@ -1523,6 +1568,71 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
|||||||
}
|
}
|
||||||
}, [fetchProjection]);
|
}, [fetchProjection]);
|
||||||
|
|
||||||
|
const controlBlockedPlanStep = useCallback(async (
|
||||||
|
step: PlanStepBusinessRow,
|
||||||
|
command: BlockedPlanStepControlCommand,
|
||||||
|
options: { input?: string } = {},
|
||||||
|
) => {
|
||||||
|
const dependency = firstActionableBlockedDependency(step);
|
||||||
|
const target = command === 'retry_task' || command === 'skip_task' ? dependency : null;
|
||||||
|
const targetStepId = target?.step_id ?? step.step_id;
|
||||||
|
const targetTaskId = target?.task_id ?? step.task_id ?? null;
|
||||||
|
const targetRunId = target?.latest_run_id ?? step.latest_run_id ?? null;
|
||||||
|
if (!targetTaskId) {
|
||||||
|
setActionError('当前阻塞步骤缺少可治理任务,无法发送处理命令');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const actionKey = `blocked-plan-step:${step.step_id}:${command}`;
|
||||||
|
setActionBusy(actionKey);
|
||||||
|
setActionError(null);
|
||||||
|
setActionNotice(null);
|
||||||
|
try {
|
||||||
|
const response = await governanceCommand({
|
||||||
|
command_kind: command,
|
||||||
|
context_refs: {
|
||||||
|
plan_id: step.plan_id,
|
||||||
|
task_id: targetTaskId,
|
||||||
|
run_id: targetRunId,
|
||||||
|
step_id: targetStepId,
|
||||||
|
blocked_step_id: step.step_id,
|
||||||
|
},
|
||||||
|
payload: {
|
||||||
|
source: 'digital-home-blocked-plan-step',
|
||||||
|
plan_id: step.plan_id,
|
||||||
|
task_id: targetTaskId,
|
||||||
|
run_id: targetRunId,
|
||||||
|
step_id: targetStepId,
|
||||||
|
blocked_step_id: step.step_id,
|
||||||
|
blocked_step_title: step.title,
|
||||||
|
blocked_dependency_step_id: target?.step_id ?? null,
|
||||||
|
requested_action: command,
|
||||||
|
input: options.input,
|
||||||
|
},
|
||||||
|
correlation_id: `digital-home-blocked-step-${step.step_id}-${command}-${Date.now()}`,
|
||||||
|
});
|
||||||
|
if (!response.accepted) {
|
||||||
|
throw new Error(response.message || `阻塞步骤处理未被接受:${response.command_id || command}`);
|
||||||
|
}
|
||||||
|
setActionNotice(`阻塞步骤已${blockedPlanStepControlLabel(command)}:${step.title}。`);
|
||||||
|
setBlockedInputTarget(null);
|
||||||
|
setBlockedInputDraft('');
|
||||||
|
await fetchProjection();
|
||||||
|
} catch (error) {
|
||||||
|
if (mountedRef.current) {
|
||||||
|
setActionError(error instanceof Error ? error.message : '阻塞步骤处理失败');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mountedRef.current) setActionBusy(null);
|
||||||
|
}
|
||||||
|
}, [fetchProjection]);
|
||||||
|
|
||||||
|
const submitBlockedPlanStepInput = useCallback(async () => {
|
||||||
|
const target = blockedInputTarget;
|
||||||
|
const input = blockedInputDraft.trim();
|
||||||
|
if (!target || !input) return;
|
||||||
|
await controlBlockedPlanStep(target.step, 'provide_task_input', { input });
|
||||||
|
}, [blockedInputDraft, blockedInputTarget, controlBlockedPlanStep]);
|
||||||
|
|
||||||
const restartService = useCallback(async (service: DigitalEmployeeManagedService) => {
|
const restartService = useCallback(async (service: DigitalEmployeeManagedService) => {
|
||||||
const actionKey = `managed-service:${service.service_id}:restart`;
|
const actionKey = `managed-service:${service.service_id}:restart`;
|
||||||
setActionBusy(actionKey);
|
setActionBusy(actionKey);
|
||||||
@@ -2115,6 +2225,72 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{(blockedPlanSteps.length > 0 || blockedPlanStepError) && (
|
||||||
|
<div className="de-task-agent-control-strip de-blocked-step-control-strip" aria-label="PlanStep 阻塞处理">
|
||||||
|
{blockedPlanStepError && <p className="de-empty-text">{blockedPlanStepError}</p>}
|
||||||
|
{blockedPlanSteps.slice(0, 3).map((step) => {
|
||||||
|
const dependency = firstActionableBlockedDependency(step);
|
||||||
|
const retryBusy = actionBusy === `blocked-plan-step:${step.step_id}:retry_task`;
|
||||||
|
const skipBusy = actionBusy === `blocked-plan-step:${step.step_id}:skip_task`;
|
||||||
|
const cancelBusy = actionBusy === `blocked-plan-step:${step.step_id}:cancel_task`;
|
||||||
|
const inputBusy = actionBusy === `blocked-plan-step:${step.step_id}:provide_task_input`;
|
||||||
|
const dependencyActionDisabled = Boolean(actionBusy) || !dependency?.task_id;
|
||||||
|
const stepActionDisabled = Boolean(actionBusy) || !step.task_id;
|
||||||
|
return (
|
||||||
|
<div key={step.step_id} className="de-task-agent-control de-blocked-step-control de-live-feed-row--danger">
|
||||||
|
<div className="de-task-agent-control-main">
|
||||||
|
<span>Blocked PlanStep</span>
|
||||||
|
<strong>{step.title}</strong>
|
||||||
|
<small>{blockedPlanStepSummary(step)}</small>
|
||||||
|
</div>
|
||||||
|
<div className="de-task-agent-control-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="de-btn-secondary de-btn-sm"
|
||||||
|
disabled={dependencyActionDisabled}
|
||||||
|
title={dependency?.task_id ? '重试首个阻塞前置任务' : '缺少可重试的前置任务'}
|
||||||
|
onClick={() => { void controlBlockedPlanStep(step, 'retry_task'); }}
|
||||||
|
>
|
||||||
|
<RefreshCw size={13} aria-hidden="true" />
|
||||||
|
<span>{retryBusy ? '重试中' : '重试前置'}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="de-btn-secondary de-btn-sm"
|
||||||
|
disabled={dependencyActionDisabled}
|
||||||
|
title={dependency?.task_id ? '跳过首个阻塞前置任务' : '缺少可跳过的前置任务'}
|
||||||
|
onClick={() => { void controlBlockedPlanStep(step, 'skip_task'); }}
|
||||||
|
>
|
||||||
|
<SkipForward size={13} aria-hidden="true" />
|
||||||
|
<span>{skipBusy ? '跳过中' : '跳过前置'}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="de-btn-secondary de-btn-sm"
|
||||||
|
disabled={stepActionDisabled}
|
||||||
|
title={step.task_id ? '补充当前阻塞步骤所需输入' : '当前步骤缺少可补充输入的任务'}
|
||||||
|
onClick={() => { setBlockedInputTarget({ step, dependency: null }); setBlockedInputDraft(''); }}
|
||||||
|
>
|
||||||
|
<MessageSquare size={13} aria-hidden="true" />
|
||||||
|
<span>{inputBusy ? '提交中' : '补充输入'}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="de-btn-secondary de-btn-sm de-task-agent-cancel-btn"
|
||||||
|
disabled={stepActionDisabled}
|
||||||
|
title={step.task_id ? '取消当前阻塞步骤' : '当前步骤缺少可取消的任务'}
|
||||||
|
onClick={() => { void controlBlockedPlanStep(step, 'cancel_task'); }}
|
||||||
|
>
|
||||||
|
<Square size={12} aria-hidden="true" />
|
||||||
|
<span>{cancelBusy ? '取消中' : '取消此步骤'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="de-stage-feed-tools">
|
<div className="de-stage-feed-tools">
|
||||||
<span>
|
<span>
|
||||||
展示 {visibleEvents.length} / 共 {allEvents.length} 条
|
展示 {visibleEvents.length} / 共 {allEvents.length} 条
|
||||||
@@ -2292,6 +2468,46 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{blockedInputTarget && (
|
||||||
|
<div className="de-task-manager-modal-overlay" role="presentation" onClick={() => setBlockedInputTarget(null)}>
|
||||||
|
<div
|
||||||
|
className="de-profile-settings-modal de-blocked-input-modal"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="补充阻塞步骤输入"
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="de-task-manager-modal-head">
|
||||||
|
<div>
|
||||||
|
<span className="de-drawer-eyebrow">BLOCKED PLANSTEP</span>
|
||||||
|
<h3>补充输入</h3>
|
||||||
|
<p>{blockedInputTarget.step.title}</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" className="de-schedule-close" onClick={() => setBlockedInputTarget(null)}>×</button>
|
||||||
|
</div>
|
||||||
|
<label className="de-blocked-input-field">
|
||||||
|
<span>补充内容</span>
|
||||||
|
<textarea
|
||||||
|
value={blockedInputDraft}
|
||||||
|
onChange={(event) => setBlockedInputDraft(event.target.value)}
|
||||||
|
placeholder="输入用于解除当前阻塞的信息"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className="de-event-modal-actions">
|
||||||
|
<button type="button" className="de-btn-secondary de-btn-sm" onClick={() => setBlockedInputTarget(null)}>取消</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="de-btn-primary de-btn-sm"
|
||||||
|
disabled={!blockedInputDraft.trim() || Boolean(actionBusy)}
|
||||||
|
onClick={() => { void submitBlockedPlanStepInput(); }}
|
||||||
|
>
|
||||||
|
提交输入
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{profileSettingsOpen && (
|
{profileSettingsOpen && (
|
||||||
<div className="de-task-manager-modal-overlay" role="presentation" onClick={() => setProfileSettingsOpen(false)}>
|
<div className="de-task-manager-modal-overlay" role="presentation" onClick={() => setProfileSettingsOpen(false)}>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -202,6 +202,46 @@ export interface PlanStepDispatchPolicy {
|
|||||||
updated_at: string | null;
|
updated_at: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PlanStepDependencyDetail {
|
||||||
|
step_id: string;
|
||||||
|
task_id: string | null;
|
||||||
|
latest_run_id?: string | null;
|
||||||
|
title: string;
|
||||||
|
status: string;
|
||||||
|
dependency_status: string | null;
|
||||||
|
dispatchable: boolean;
|
||||||
|
skip_reason: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlanStepBusinessRow {
|
||||||
|
step_id: string;
|
||||||
|
node_id?: string;
|
||||||
|
plan_id: string | null;
|
||||||
|
title: string;
|
||||||
|
status: string;
|
||||||
|
seq?: number | null;
|
||||||
|
task_id?: string | null;
|
||||||
|
latest_run_id?: string | null;
|
||||||
|
depends_on: string[];
|
||||||
|
dependency_status: string | null;
|
||||||
|
blocked_dependencies: string[];
|
||||||
|
waiting_dependencies: string[];
|
||||||
|
satisfied_dependencies: string[];
|
||||||
|
blocked_dependency_details?: PlanStepDependencyDetail[];
|
||||||
|
dispatchable: boolean;
|
||||||
|
skip_reason: string | null;
|
||||||
|
sync_status?: string | null;
|
||||||
|
updated_at?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BusinessViewResponse<T> {
|
||||||
|
items: T[];
|
||||||
|
page_no: number;
|
||||||
|
page_size: number;
|
||||||
|
total: number;
|
||||||
|
source?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type ChatRouteMode = 'auto' | 'chat' | 'direct' | 'plan';
|
export type ChatRouteMode = 'auto' | 'chat' | 'direct' | 'plan';
|
||||||
|
|
||||||
export interface WsMessage {
|
export interface WsMessage {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -16,8 +16,8 @@
|
|||||||
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
|
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script type="module" crossorigin src="./assets/index-Dar5kQHh.js"></script>
|
<script type="module" crossorigin src="./assets/index-BRZXqRQH.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-DqdYYbRq.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-4TVkOCsf.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -288,12 +288,16 @@ function checkDigitalPlanStepGraphDispatchPolicy() {
|
|||||||
const adapterPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "lib", "qimingclawAdapter.ts");
|
const adapterPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "lib", "qimingclawAdapter.ts");
|
||||||
const apiPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "lib", "api.ts");
|
const apiPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "lib", "api.ts");
|
||||||
const typesPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "types", "api.ts");
|
const typesPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "types", "api.ts");
|
||||||
|
const commandDeckPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "pages", "digital", "CommandDeck.tsx");
|
||||||
|
const cssPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "index.css");
|
||||||
const stateServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "stateService.ts");
|
const stateServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "stateService.ts");
|
||||||
const schedulerServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "schedulerService.ts");
|
const schedulerServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "schedulerService.ts");
|
||||||
const syncServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "syncService.ts");
|
const syncServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "syncService.ts");
|
||||||
const adapter = fs.readFileSync(adapterPath, "utf8");
|
const adapter = fs.readFileSync(adapterPath, "utf8");
|
||||||
const api = fs.readFileSync(apiPath, "utf8");
|
const api = fs.readFileSync(apiPath, "utf8");
|
||||||
const types = fs.readFileSync(typesPath, "utf8");
|
const types = fs.readFileSync(typesPath, "utf8");
|
||||||
|
const commandDeck = fs.readFileSync(commandDeckPath, "utf8");
|
||||||
|
const css = fs.readFileSync(cssPath, "utf8");
|
||||||
const stateService = fs.readFileSync(stateServicePath, "utf8");
|
const stateService = fs.readFileSync(stateServicePath, "utf8");
|
||||||
const schedulerService = fs.readFileSync(schedulerServicePath, "utf8");
|
const schedulerService = fs.readFileSync(schedulerServicePath, "utf8");
|
||||||
const syncService = fs.readFileSync(syncServicePath, "utf8");
|
const syncService = fs.readFileSync(syncServicePath, "utf8");
|
||||||
@@ -304,12 +308,23 @@ function checkDigitalPlanStepGraphDispatchPolicy() {
|
|||||||
[adapter, "planStepRows", "management plan step rows"],
|
[adapter, "planStepRows", "management plan step rows"],
|
||||||
[adapter, "planStepGraphRows", "management plan step graph rows"],
|
[adapter, "planStepGraphRows", "management plan step graph rows"],
|
||||||
[adapter, "filterPlanStepRows", "management plan step filters"],
|
[adapter, "filterPlanStepRows", "management plan step filters"],
|
||||||
|
[adapter, "blocked_dependency_details", "plan step blocked dependency details"],
|
||||||
|
[adapter, "planStepDependencyDetails", "plan step blocked dependency detail mapper"],
|
||||||
[adapter, "update_plan_step_dispatch_policy", "adapter plan step dispatch policy save action"],
|
[adapter, "update_plan_step_dispatch_policy", "adapter plan step dispatch policy save action"],
|
||||||
[api, "getPlanStepDispatchPolicy", "plan step dispatch policy API getter"],
|
[api, "getPlanStepDispatchPolicy", "plan step dispatch policy API getter"],
|
||||||
[api, "patchPlanStepDispatchPolicy", "plan step dispatch policy API patcher"],
|
[api, "patchPlanStepDispatchPolicy", "plan step dispatch policy API patcher"],
|
||||||
[api, "getPlanSteps", "plan steps API getter"],
|
[api, "getPlanSteps", "plan steps API getter"],
|
||||||
[api, "getPlanStepGraph", "plan step graph API getter"],
|
[api, "getPlanStepGraph", "plan step graph API getter"],
|
||||||
|
[api, "BusinessViewResponse<PlanStepBusinessRow>", "typed plan step business view API"],
|
||||||
[types, "PlanStepDispatchPolicy", "plan step dispatch policy type"],
|
[types, "PlanStepDispatchPolicy", "plan step dispatch policy type"],
|
||||||
|
[types, "PlanStepDependencyDetail", "plan step blocked dependency detail type"],
|
||||||
|
[types, "PlanStepBusinessRow", "plan step business row type"],
|
||||||
|
[types, "BusinessViewResponse", "business view response type"],
|
||||||
|
[commandDeck, "getPlanStepGraph", "home blocked plan step graph fetch"],
|
||||||
|
[commandDeck, "controlBlockedPlanStep", "home blocked plan step control handler"],
|
||||||
|
[commandDeck, "de-blocked-step-control-strip", "home blocked plan step control strip"],
|
||||||
|
[commandDeck, "provide_task_input", "home blocked plan step input command"],
|
||||||
|
[css, "de-blocked-input-field", "home blocked plan step input styles"],
|
||||||
[stateService, "planStepDispatchPolicy", "plan step dispatch policy UI state"],
|
[stateService, "planStepDispatchPolicy", "plan step dispatch policy UI state"],
|
||||||
[stateService, "update_plan_step_dispatch_policy", "plan step dispatch policy runtime event"],
|
[stateService, "update_plan_step_dispatch_policy", "plan step dispatch policy runtime event"],
|
||||||
[schedulerService, "readDigitalEmployeePlanStepDispatchPolicy", "scheduler plan step dispatch policy reader"],
|
[schedulerService, "readDigitalEmployeePlanStepDispatchPolicy", "scheduler plan step dispatch policy reader"],
|
||||||
|
|||||||
@@ -540,9 +540,9 @@ GET /api/digital-employee/approvals
|
|||||||
|
|
||||||
1. **PlanStep 与依赖图(依赖图调度已开始)**
|
1. **PlanStep 与依赖图(依赖图调度已开始)**
|
||||||
- 已吸收:Plan / Task / Run 记录、计划模板、`create_plan` 中的 steps/tasks 可物化为任务;PlanStep 已开始作为正式本地实体落入 `digital_plan_steps`。
|
- 已吸收:Plan / Task / Run 记录、计划模板、`create_plan` 中的 steps/tasks 可物化为任务;PlanStep 已开始作为正式本地实体落入 `digital_plan_steps`。
|
||||||
- 当前能力:PlanStep 会进入 `digital_sync_outbox`,entity type 为 `plan_step`;任务中心 flow 和 debug store 优先读取真实 PlanStep 生成步骤图;Task Agent 的 retry / skip / cancel / pause / resume / input / run 控制已可更新真实 Task、PlanStep、Run、Event 和 outbox;前置步骤跳过、取消或重试后会重新评估后继步骤,推进为 ready / blocked / pending 并写入依赖图事件;scheduler sweep 会识别可派发的 ready PlanStep,调用本地 `/computer/chat` 并写入 `plan_step_dispatch_*` 与 `governance_start_run` 事件;embedded 已提供 `/api/digital-employee/plan-steps`、`/api/digital-employee/plan-step-graph` 和 `/api/plan-step/dispatch-policy`,ready PlanStep 安全派发、管理端依赖图视图和本地派发策略已开始闭环。
|
- 当前能力:PlanStep 会进入 `digital_sync_outbox`,entity type 为 `plan_step`;任务中心 flow 和 debug store 优先读取真实 PlanStep 生成步骤图;Task Agent 的 retry / skip / cancel / pause / resume / input / run 控制已可更新真实 Task、PlanStep、Run、Event 和 outbox;前置步骤跳过、取消或重试后会重新评估后继步骤,推进为 ready / blocked / pending 并写入依赖图事件;scheduler sweep 会识别可派发的 ready PlanStep,调用本地 `/computer/chat` 并写入 `plan_step_dispatch_*` 与 `governance_start_run` 事件;embedded 已提供 `/api/digital-employee/plan-steps`、`/api/digital-employee/plan-step-graph` 和 `/api/plan-step/dispatch-policy`,ready PlanStep 安全派发、管理端依赖图视图、本地派发策略和首页阻塞依赖处理入口已开始闭环。
|
||||||
- 后续缺口:跨任务编排执行、依赖阻塞人工处理 UI、多设备 lease 和远端策略拉取仍待吸收。
|
- 后续缺口:跨任务编排执行、多设备 lease 和远端策略拉取仍待吸收。
|
||||||
- 建议:下一轮围绕依赖阻塞人工处理 UI 或跨任务编排,把本地可治理调度推进到更完整的执行控制面。
|
- 建议:下一轮围绕跨任务编排或多设备 lease,把本地可治理调度推进到更完整的执行控制面。
|
||||||
|
|
||||||
2. **调度 / 定时 / 周期任务**
|
2. **调度 / 定时 / 周期任务**
|
||||||
- 已吸收:`digital_schedules`、`digital_schedule_runs` 已进入 qimingclaw 本地 SQLite 与 outbox,entity type 为 `schedule` / `schedule_run`;旧 `/api/cron`、`/api/scheduler/jobs` 和治理命令会优先读取真实本地调度记录。
|
- 已吸收:`digital_schedules`、`digital_schedule_runs` 已进入 qimingclaw 本地 SQLite 与 outbox,entity type 为 `schedule` / `schedule_run`;旧 `/api/cron`、`/api/scheduler/jobs` 和治理命令会优先读取真实本地调度记录。
|
||||||
|
|||||||
Reference in New Issue
Block a user