吸收数字员工安全服务重启入口

This commit is contained in:
baiyanyun
2026-06-09 23:10:01 +08:00
parent 0d2008b35c
commit 91540dec0c
13 changed files with 717 additions and 155 deletions

View File

@@ -0,0 +1,187 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { HandlerContext } from "@shared/types/ipc";
const mockStateService = vi.hoisted(() => ({
recordManagedServiceAction: vi.fn(),
}));
const mockServiceManager = vi.hoisted(() => ({
createServiceManager: vi.fn(),
startFileServer: vi.fn(),
}));
vi.mock("electron", () => ({
ipcMain: { handle: vi.fn() },
}));
vi.mock("@shared/featureFlags", () => ({
FEATURES: { ENABLE_GUI_AGENT_SERVER: true },
}));
vi.mock("../window/serviceManager", () => ({
createServiceManager: mockServiceManager.createServiceManager,
}));
vi.mock("../services/startupPorts", () => ({
getConfiguredPorts: () => ({ fileServer: 61234 }),
}));
vi.mock("../services/system/shellEnv", () => ({
isWindows: () => false,
}));
vi.mock("../services/packages/guiAgentServer", () => ({
getGuiAgentServerStatus: () => ({ running: true }),
startGuiAgentServer: vi.fn(async () => ({ success: true })),
stopGuiAgentServer: vi.fn(async () => ({ success: true })),
}));
vi.mock("../services/packages/windowsMcp", () => ({
getWindowsMcpStatus: () => ({ running: false }),
startWindowsMcp: vi.fn(async () => ({ success: true })),
stopWindowsMcp: vi.fn(async () => ({ success: true })),
}));
vi.mock("../services/engines/unifiedAgent", () => ({
agentService: {
isReady: true,
getEngineType: () => "acp",
listAllSessionsDetailed: () => [],
},
}));
vi.mock("../services/packages/mcp", () => ({
mcpProxyManager: { getStatus: () => ({ running: true }) },
}));
vi.mock("../services/computerServer", () => ({
getComputerServerStatus: () => ({ running: true }),
}));
vi.mock("../services/digitalEmployee/stateService", () => ({
recordDigitalEmployeeSnapshot: vi.fn(() => ({ updatedAt: null, events: [] })),
recordDigitalEmployeeGovernanceCommand: vi.fn(),
recordDigitalEmployeeDailyReport: vi.fn(),
recordDigitalEmployeeManagedServiceAction: mockStateService.recordManagedServiceAction,
readDigitalEmployeeRuntimeRecords: vi.fn(),
readDigitalEmployeeState: vi.fn(),
readDigitalEmployeeUiState: vi.fn(),
readDigitalEmployeeCronSettings: vi.fn(),
saveDigitalEmployeeCronSettings: vi.fn(),
readDigitalEmployeeScheduleRuns: vi.fn(),
saveDigitalEmployeeUiState: vi.fn(),
listDigitalEmployeeMemories: vi.fn(),
upsertDigitalEmployeeMemory: vi.fn(),
deleteDigitalEmployeeMemory: vi.fn(),
syncQimingclawMemoryEntries: vi.fn(),
recordDigitalEmployeeRouteDecision: vi.fn(),
listDigitalEmployeeRouteDecisions: vi.fn(),
}));
vi.mock("../services/digitalEmployee/schedulerService", () => ({
runDigitalEmployeeScheduleNow: vi.fn(),
runDigitalEmployeeSchedulerCheck: vi.fn(),
}));
vi.mock("../services/digitalEmployee/syncService", () => ({
flushDigitalEmployeeSyncOutbox: vi.fn(),
getDigitalEmployeeSyncStatus: vi.fn(),
}));
vi.mock("../services/digitalEmployee/planTemplateService", () => ({
listDigitalEmployeePlanTemplates: vi.fn(),
}));
vi.mock("../services/digitalEmployee/skillCatalogService", () => ({
listDigitalEmployeeSkillCapabilities: vi.fn(),
setDigitalEmployeeSkillEnabled: vi.fn(),
}));
vi.mock("../services/memory", () => ({
memoryService: {
isInitialized: () => false,
listMemories: vi.fn(),
addMemory: vi.fn(),
deleteMemory: vi.fn(),
},
}));
function createCtx(): HandlerContext {
return {
getMainWindow: () => null,
lanproxy: { status: () => ({ running: true }) } as any,
fileServer: {
stop: vi.fn(() => ({ success: true })),
status: vi.fn(() => ({ running: false, port: 61234 })),
} as any,
agentRunner: {} as any,
guiServer: {} as any,
agentRunnerPorts: null,
setAgentRunnerPorts: vi.fn(),
};
}
describe("digital employee managed service actions", () => {
beforeEach(() => {
vi.clearAllMocks();
mockStateService.recordManagedServiceAction.mockReturnValue({ eventId: "event-1" });
mockServiceManager.startFileServer.mockResolvedValue({ success: true, message: "started" });
mockServiceManager.createServiceManager.mockReturnValue({
startFileServer: mockServiceManager.startFileServer,
});
});
it("restarts allowlisted file server without touching core services", async () => {
const { restartManagedService } = await import("./digitalEmployeeHandlers");
const ctx = createCtx();
const result = await restartManagedService(ctx, { serviceId: "fileServer", reason: "test", actor: "operator" });
expect(ctx.fileServer.stop).toHaveBeenCalledTimes(1);
expect(mockServiceManager.createServiceManager).toHaveBeenCalledWith({
lanproxy: ctx.lanproxy,
fileServer: ctx.fileServer,
agentRunner: ctx.agentRunner,
});
expect(mockServiceManager.startFileServer).toHaveBeenCalledWith(61234);
expect(result).toMatchObject({ ok: true, service_id: "fileServer", status: "success", event_id: "event-1" });
expect(mockStateService.recordManagedServiceAction).toHaveBeenCalledWith(expect.objectContaining({
serviceId: "fileServer",
action: "restart",
status: "success",
allowlisted: true,
}));
});
it("rejects core service restart requests", async () => {
const { restartManagedService } = await import("./digitalEmployeeHandlers");
const ctx = createCtx();
const result = await restartManagedService(ctx, { serviceId: "computerServer" });
expect(ctx.fileServer.stop).not.toHaveBeenCalled();
expect(mockServiceManager.createServiceManager).not.toHaveBeenCalled();
expect(result).toMatchObject({ ok: false, service_id: "computerServer", status: "rejected", error: "service_restart_not_allowed" });
expect(mockStateService.recordManagedServiceAction).toHaveBeenCalledWith(expect.objectContaining({
serviceId: "computerServer",
status: "rejected",
allowlisted: false,
}));
});
it("returns failed status when allowlisted restart fails", async () => {
const { restartManagedService } = await import("./digitalEmployeeHandlers");
const ctx = createCtx();
mockServiceManager.startFileServer.mockResolvedValue({ success: false, error: "port busy" });
const result = await restartManagedService(ctx, { serviceId: "fileServer" });
expect(result).toMatchObject({ ok: false, service_id: "fileServer", status: "failed", error: "port busy" });
expect(mockStateService.recordManagedServiceAction).toHaveBeenCalledWith(expect.objectContaining({
serviceId: "fileServer",
status: "failed",
allowlisted: true,
result: expect.objectContaining({ error: "port busy" }),
}));
});
});

View File

@@ -4,13 +4,24 @@ import { FEATURES } from "@shared/featureFlags";
import { agentService } from "../services/engines/unifiedAgent";
import { mcpProxyManager } from "../services/packages/mcp";
import { getComputerServerStatus } from "../services/computerServer";
import { getGuiAgentServerStatus } from "../services/packages/guiAgentServer";
import { getWindowsMcpStatus } from "../services/packages/windowsMcp";
import {
getGuiAgentServerStatus,
startGuiAgentServer,
stopGuiAgentServer,
} from "../services/packages/guiAgentServer";
import {
getWindowsMcpStatus,
startWindowsMcp,
stopWindowsMcp,
} from "../services/packages/windowsMcp";
import { isWindows } from "../services/system/shellEnv";
import { createServiceManager } from "../window/serviceManager";
import { getConfiguredPorts } from "../services/startupPorts";
import {
recordDigitalEmployeeSnapshot,
recordDigitalEmployeeGovernanceCommand,
recordDigitalEmployeeDailyReport,
recordDigitalEmployeeManagedServiceAction,
readDigitalEmployeeRuntimeRecords,
readDigitalEmployeeState,
readDigitalEmployeeUiState,
@@ -25,6 +36,7 @@ import {
recordDigitalEmployeeRouteDecision,
listDigitalEmployeeRouteDecisions,
type DigitalEmployeeManagedService,
type DigitalEmployeeManagedServiceActionInput,
type DigitalEmployeeGovernanceCommandRecord,
type DigitalEmployeeDailyReportRecordInput,
type DigitalEmployeeMemoryUpsertInput,
@@ -48,6 +60,26 @@ import {
} from "../services/digitalEmployee/skillCatalogService";
import { memoryService } from "../services/memory";
interface RestartManagedServiceRequest {
serviceId?: string;
service_id?: string;
reason?: string | null;
actor?: string | null;
}
interface RestartManagedServiceResponse {
ok: boolean;
service_id: string;
action: "restart";
status: "success" | "failed" | "rejected";
message?: string;
error?: string;
managed_service?: DigitalEmployeeManagedService | null;
event_id?: string | null;
}
const RESTARTABLE_MANAGED_SERVICES = new Set(["fileServer", "guiServer"]);
export function registerDigitalEmployeeHandlers(ctx: HandlerContext): void {
ipcMain.handle("digitalEmployee:getSnapshot", async () => {
const snapshot = buildSnapshot(ctx);
@@ -188,6 +220,105 @@ export function registerDigitalEmployeeHandlers(ctx: HandlerContext): void {
return recordDigitalEmployeeDailyReport(input);
},
);
ipcMain.handle(
"digitalEmployee:restartManagedService",
async (_, input: RestartManagedServiceRequest) => {
return restartManagedService(ctx, input ?? {});
},
);
}
export async function restartManagedService(
ctx: HandlerContext,
input: RestartManagedServiceRequest,
): Promise<RestartManagedServiceResponse> {
const serviceId = String(input.serviceId || input.service_id || "").trim();
const base = {
service_id: serviceId,
action: "restart" as const,
};
if (!RESTARTABLE_MANAGED_SERVICES.has(serviceId)) {
const event = recordManagedServiceAction({
serviceId,
action: "restart",
status: "rejected",
reason: input.reason,
actor: input.actor,
allowlisted: false,
result: { error: "service_restart_not_allowed" },
});
return {
...base,
ok: false,
status: "rejected",
error: "service_restart_not_allowed",
managed_service: managedServiceById(ctx, serviceId),
event_id: event?.eventId ?? null,
};
}
let result: { success?: boolean; error?: string; message?: string };
try {
if (serviceId === "fileServer") {
ctx.fileServer.stop();
const serviceManager = createServiceManager({
lanproxy: ctx.lanproxy,
fileServer: ctx.fileServer,
agentRunner: ctx.agentRunner,
});
result = await serviceManager.startFileServer(getConfiguredPorts().fileServer);
} else {
result = await restartGuiManagedService();
}
} catch (error) {
result = { success: false, error: error instanceof Error ? error.message : String(error) };
}
const status = result.success ? "success" : "failed";
const event = recordManagedServiceAction({
serviceId,
action: "restart",
status,
reason: input.reason,
actor: input.actor,
allowlisted: true,
result: result as Record<string, unknown>,
});
return {
...base,
ok: Boolean(result.success),
status,
message: result.message,
error: result.error,
managed_service: managedServiceById(ctx, serviceId),
event_id: event?.eventId ?? null,
};
}
async function restartGuiManagedService(): Promise<{ success: boolean; error?: string; message?: string }> {
if (!FEATURES.ENABLE_GUI_AGENT_SERVER) {
return { success: false, error: "gui_service_not_available" };
}
if (isWindows()) {
await stopWindowsMcp();
return startWindowsMcp();
}
await stopGuiAgentServer();
return startGuiAgentServer();
}
function recordManagedServiceAction(
input: DigitalEmployeeManagedServiceActionInput,
): ReturnType<typeof recordDigitalEmployeeManagedServiceAction> {
return recordDigitalEmployeeManagedServiceAction(input);
}
function managedServiceById(
ctx: HandlerContext,
serviceId: string,
): DigitalEmployeeManagedService | null {
return buildSnapshot(ctx).managedServices?.find((service) => service.serviceId === serviceId) ?? null;
}
function buildSnapshot(ctx: HandlerContext): DigitalEmployeeSnapshot {