吸收数字员工安全服务重启入口
This commit is contained in:
@@ -26,6 +26,7 @@ import type {
|
||||
IngressRouteResponse,
|
||||
GovernanceCommandRequest,
|
||||
GovernanceCommandResponse,
|
||||
DigitalEmployeeManagedService,
|
||||
DigitalEmployeeWorkdayProjection,
|
||||
DigitalEmployeeWorkdayActionRequest,
|
||||
PlanDispatchResponse,
|
||||
@@ -613,6 +614,26 @@ export function postDigitalEmployeeWorkdayAction(
|
||||
});
|
||||
}
|
||||
|
||||
export function restartManagedService(
|
||||
serviceId: string,
|
||||
body: { reason?: string; actor?: string } = {},
|
||||
): Promise<{
|
||||
ok: boolean;
|
||||
service_id: string;
|
||||
action: 'restart';
|
||||
status: 'success' | 'failed' | 'rejected';
|
||||
message?: string;
|
||||
error?: string;
|
||||
managed_service?: DigitalEmployeeManagedService | null;
|
||||
event_id?: string | null;
|
||||
}> {
|
||||
return apiFetch(`/api/digital-employee/managed-services/${encodeURIComponent(serviceId)}/restart`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
timeoutMs: DIGITAL_EMPLOYEE_WORKDAY_TIMEOUT_MS,
|
||||
});
|
||||
}
|
||||
|
||||
export function dispatchPlanNow(planId: string): Promise<PlanDispatchResponse> {
|
||||
return apiFetch<PlanDispatchResponse>(`/api/debug/plan/${encodeURIComponent(planId)}/dispatch`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -63,6 +63,7 @@ declare global {
|
||||
recordRouteDecision?: (input: QimingclawRouteDecisionInput) => Promise<RouteDecisionRecord>;
|
||||
recordGovernanceCommand?: (command: QimingclawGovernanceCommandRecord) => Promise<unknown>;
|
||||
recordDailyReport?: (input: QimingclawDailyReportRecordInput) => Promise<QimingclawDailyReportRecordResult | null>;
|
||||
restartManagedService?: (serviceId: string, options?: { reason?: string | null; actor?: string | null }) => Promise<QimingclawManagedServiceActionResponse>;
|
||||
respondPermission?: (sessionId: string, permissionId: string, response: 'once' | 'always' | 'reject') => Promise<{ success?: boolean; error?: string }>;
|
||||
};
|
||||
};
|
||||
@@ -236,6 +237,17 @@ interface QimingclawDailyReportRecordResult {
|
||||
eventId: string;
|
||||
}
|
||||
|
||||
interface QimingclawManagedServiceActionResponse {
|
||||
ok: boolean;
|
||||
service_id: string;
|
||||
action: 'restart';
|
||||
status: 'success' | 'failed' | 'rejected';
|
||||
message?: string;
|
||||
error?: string;
|
||||
managed_service?: DigitalEmployeeManagedService | null;
|
||||
event_id?: string | null;
|
||||
}
|
||||
|
||||
interface QimingclawRouteDecisionInput {
|
||||
id?: string | null;
|
||||
idempotencyKey?: string | null;
|
||||
@@ -625,6 +637,13 @@ export async function handleQimingclawDigitalApi<T>(
|
||||
if (method === 'POST' && pathname === '/api/digital-employee/workday/actions') {
|
||||
return await handleWorkdayAction(parseJsonBody<DigitalEmployeeWorkdayActionRequest>(options.body), await readQimingclawSnapshot()) as T;
|
||||
}
|
||||
const managedServiceRestartMatch = pathname.match(/^\/api\/(?:digital-employee\/)?managed-services\/([^/]+)\/restart$/);
|
||||
if (method === 'POST' && managedServiceRestartMatch) {
|
||||
return await handleManagedServiceRestart(
|
||||
decodeURIComponent(managedServiceRestartMatch[1] || ''),
|
||||
parseOptionalJsonBody(options.body),
|
||||
) as T;
|
||||
}
|
||||
const dailyReportDetailMatch = pathname.match(/^\/api\/digital-employee\/daily-reports\/([^/]+)$/);
|
||||
if (method === 'GET' && dailyReportDetailMatch) {
|
||||
const reportId = decodeURIComponent(dailyReportDetailMatch[1] || '');
|
||||
@@ -1289,6 +1308,45 @@ function managedServiceSummary(services: DigitalEmployeeManagedService[]): strin
|
||||
return `ManagedService:${runningCount}/${services.length} 运行,${issueCount} 项需处理`;
|
||||
}
|
||||
|
||||
async function handleManagedServiceRestart(
|
||||
serviceId: string,
|
||||
body: Record<string, unknown>,
|
||||
): Promise<QimingclawManagedServiceActionResponse> {
|
||||
const bridge = window.QimingClawBridge?.digital;
|
||||
if (!bridge?.restartManagedService) {
|
||||
const snapshot = await readQimingclawSnapshot();
|
||||
return {
|
||||
ok: false,
|
||||
service_id: serviceId,
|
||||
action: 'restart',
|
||||
status: 'failed',
|
||||
error: 'bridge_unavailable',
|
||||
managed_service: buildManagedServices(snapshot).find((service) => service.service_id === serviceId) ?? null,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const result = await bridge.restartManagedService(serviceId, {
|
||||
reason: stringValue(body.reason) || 'digital_employee_managed_service_restart',
|
||||
actor: stringValue(body.actor) || 'digital_employee_ui',
|
||||
});
|
||||
const snapshot = await readQimingclawSnapshot();
|
||||
return {
|
||||
...result,
|
||||
managed_service: result.managed_service ?? buildManagedServices(snapshot).find((service) => service.service_id === serviceId) ?? null,
|
||||
};
|
||||
} catch (error) {
|
||||
const snapshot = await readQimingclawSnapshot();
|
||||
return {
|
||||
ok: false,
|
||||
service_id: serviceId,
|
||||
action: 'restart',
|
||||
status: 'failed',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
managed_service: buildManagedServices(snapshot).find((service) => service.service_id === serviceId) ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function syncLine(snapshot: QimingclawSnapshot | null): string {
|
||||
const sync = snapshot?.sync;
|
||||
if (!sync) return '管理端同步:未连接';
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
getSchedulerJobs,
|
||||
governanceCommand,
|
||||
postDigitalEmployeeWorkdayAction,
|
||||
restartManagedService,
|
||||
} from '@/lib/api';
|
||||
import { isTauri } from '@/lib/tauri';
|
||||
import type {
|
||||
@@ -720,6 +721,10 @@ function managedServiceSummary(service: DigitalEmployeeManagedService): string {
|
||||
return `${service.name} ${service.status_label}${service.error ? `:${service.error}` : deps}`;
|
||||
}
|
||||
|
||||
function canRestartManagedService(service: DigitalEmployeeManagedService): boolean {
|
||||
return service.service_id === 'fileServer' || service.service_id === 'guiServer';
|
||||
}
|
||||
|
||||
function taskAgentSummary(agent: DigitalEmployeeTaskAgentState): string {
|
||||
const next = agent.next_action === 'retry_or_skip' ? '重试或跳过' : agent.next_action === 'watch_progress' ? '观察进度' : agent.next_action;
|
||||
return `${agent.title} ${agent.status_label},下一步 ${next}`;
|
||||
@@ -1387,6 +1392,30 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
||||
}
|
||||
}, [fetchProjection]);
|
||||
|
||||
const restartService = useCallback(async (service: DigitalEmployeeManagedService) => {
|
||||
const actionKey = `managed-service:${service.service_id}:restart`;
|
||||
setActionBusy(actionKey);
|
||||
setActionError(null);
|
||||
setActionNotice(null);
|
||||
try {
|
||||
const response = await restartManagedService(service.service_id, {
|
||||
reason: 'digital_home_managed_service_issue',
|
||||
actor: 'digital_employee_ui',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(response.error || response.message || `${service.name} 重启失败`);
|
||||
}
|
||||
setActionNotice(`${service.name} 已提交安全重启。`);
|
||||
await fetchProjection();
|
||||
} catch (error) {
|
||||
if (mountedRef.current) {
|
||||
setActionError(error instanceof Error ? error.message : `${service.name} 重启失败`);
|
||||
}
|
||||
} finally {
|
||||
if (mountedRef.current) setActionBusy(null);
|
||||
}
|
||||
}, [fetchProjection]);
|
||||
|
||||
const openDuty = useCallback((planItem: DigitalPlanItem) => {
|
||||
onNavigate({ tab: 'missions', date: selectedDate, missionId: planItem.conversationId || planItem.id });
|
||||
}, [onNavigate, selectedDate]);
|
||||
@@ -1679,6 +1708,37 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
||||
{actionError || actionNotice || planDataError || workdayProjection?.demo_reason}
|
||||
</div>
|
||||
)}
|
||||
{managedServiceIssues.length > 0 && (
|
||||
<div className="de-management-sync-failures" aria-label="服务状态处理" style={{ marginBottom: 10 }}>
|
||||
{managedServiceIssues.map((service) => {
|
||||
const actionKey = `managed-service:${service.service_id}:restart`;
|
||||
const restartable = canRestartManagedService(service);
|
||||
return (
|
||||
<div key={service.service_id} className="de-management-sync-failure" title={service.error || managedServiceSummary(service)}>
|
||||
<span>{service.kind}</span>
|
||||
<strong>{service.name}</strong>
|
||||
<em>{service.status_label}</em>
|
||||
<small>{managedServiceSummary(service)}</small>
|
||||
{restartable && (
|
||||
<button
|
||||
type="button"
|
||||
className="de-btn-secondary de-btn-sm"
|
||||
disabled={actionBusy === actionKey}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
void restartService(service);
|
||||
}}
|
||||
title={`安全重启 ${service.name}`}
|
||||
>
|
||||
<RefreshCw size={13} aria-hidden="true" />
|
||||
<span>{actionBusy === actionKey ? '重启中...' : '重启'}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{selectionDirty && !actionNotice && !actionError && (
|
||||
<div className="de-schedule-result de-schedule-result--success" style={{ marginBottom: 10 }}>
|
||||
当前选择尚未确认,自动刷新不会覆盖你的勾选。
|
||||
|
||||
Reference in New Issue
Block a user