Files
qiming/qimingclaw/crates/agent-electron-client/scripts/check-digital-employee.js
2026-06-11 18:31:41 +08:00

692 lines
42 KiB
JavaScript

#!/usr/bin/env node
const fs = require("fs");
const os = require("os");
const path = require("path");
const { spawnSync } = require("child_process");
const packageRoot = path.resolve(__dirname, "..");
const requiredDigitalTables = [
"digital_plans",
"digital_plan_steps",
"digital_schedules",
"digital_schedule_runs",
"digital_tasks",
"digital_runs",
"digital_events",
"digital_artifacts",
"digital_approvals",
"digital_memories",
"digital_sync_outbox",
"digital_sync_attempts",
];
const testFiles = [
"src/main/services/digitalEmployee/stateService.test.ts",
"src/main/services/digitalEmployee/artifactAccessService.test.ts",
"src/main/services/digitalEmployee/schedulerService.test.ts",
"src/main/ipc/digitalEmployeeHandlers.test.ts",
"src/main/ipc/eventForwarders.test.ts",
"src/main/services/engines/acp/acpEngine.test.ts",
"src/main/services/digitalEmployee/syncService.test.ts",
"src/main/services/digitalEmployee/skillCatalogService.test.ts",
"src/main/services/digitalEmployee/planTemplateService.test.ts",
];
const selfHealingDigitalSchemaSql = `
CREATE TABLE IF NOT EXISTS digital_plan_steps (
id TEXT PRIMARY KEY,
remote_id TEXT,
plan_id TEXT NOT NULL,
title TEXT NOT NULL,
status TEXT NOT NULL,
seq INTEGER NOT NULL,
depends_on TEXT NOT NULL DEFAULT '[]',
preferred_skill_ids TEXT NOT NULL DEFAULT '[]',
payload TEXT NOT NULL DEFAULT '{}',
sync_status TEXT NOT NULL DEFAULT 'pending',
last_synced_at TEXT,
sync_error TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY(plan_id) REFERENCES digital_plans(id)
);
CREATE INDEX IF NOT EXISTS idx_digital_plan_steps_plan ON digital_plan_steps(plan_id, seq);
CREATE INDEX IF NOT EXISTS idx_digital_plan_steps_sync ON digital_plan_steps(sync_status);
CREATE TABLE IF NOT EXISTS digital_schedules (
id TEXT PRIMARY KEY,
remote_id TEXT,
plan_id TEXT,
name TEXT NOT NULL,
description TEXT,
cron_expr TEXT NOT NULL,
timezone TEXT NOT NULL DEFAULT 'system',
status TEXT NOT NULL DEFAULT 'enabled',
enabled INTEGER NOT NULL DEFAULT 1,
prompt TEXT,
command TEXT,
payload TEXT NOT NULL DEFAULT '{}',
next_run_at TEXT,
last_run_at TEXT,
catch_up_on_startup INTEGER NOT NULL DEFAULT 1,
max_catch_up_runs INTEGER NOT NULL DEFAULT 5,
max_run_history INTEGER NOT NULL DEFAULT 100,
failure_count INTEGER NOT NULL DEFAULT 0,
last_error TEXT,
sync_status TEXT NOT NULL DEFAULT 'pending',
last_synced_at TEXT,
sync_error TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
deleted_at TEXT,
FOREIGN KEY(plan_id) REFERENCES digital_plans(id)
);
CREATE TABLE IF NOT EXISTS digital_schedule_runs (
id TEXT PRIMARY KEY,
remote_id TEXT,
schedule_id TEXT NOT NULL,
plan_id TEXT,
run_id TEXT,
due_at TEXT NOT NULL,
started_at TEXT,
finished_at TEXT,
status TEXT NOT NULL,
attempt_no INTEGER NOT NULL DEFAULT 1,
trigger_kind TEXT NOT NULL DEFAULT 'scheduled',
error TEXT,
payload TEXT NOT NULL DEFAULT '{}',
sync_status TEXT NOT NULL DEFAULT 'pending',
last_synced_at TEXT,
sync_error TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY(schedule_id) REFERENCES digital_schedules(id),
FOREIGN KEY(plan_id) REFERENCES digital_plans(id),
FOREIGN KEY(run_id) REFERENCES digital_runs(id)
);
CREATE INDEX IF NOT EXISTS idx_digital_schedules_due ON digital_schedules(enabled, status, next_run_at);
CREATE INDEX IF NOT EXISTS idx_digital_schedules_plan ON digital_schedules(plan_id);
CREATE INDEX IF NOT EXISTS idx_digital_schedules_sync ON digital_schedules(sync_status);
CREATE INDEX IF NOT EXISTS idx_digital_schedule_runs_schedule ON digital_schedule_runs(schedule_id, due_at);
CREATE UNIQUE INDEX IF NOT EXISTS idx_digital_schedule_runs_attempt ON digital_schedule_runs(schedule_id, due_at, attempt_no);
CREATE INDEX IF NOT EXISTS idx_digital_schedule_runs_sync ON digital_schedule_runs(sync_status);
CREATE TABLE IF NOT EXISTS digital_memories (
id TEXT PRIMARY KEY,
remote_id TEXT,
key TEXT NOT NULL,
content TEXT NOT NULL,
category TEXT NOT NULL DEFAULT 'fact',
source TEXT NOT NULL DEFAULT 'qimingclaw',
source_path TEXT,
session_id TEXT,
score REAL,
status TEXT NOT NULL DEFAULT 'active',
payload TEXT NOT NULL DEFAULT '{}',
sync_status TEXT NOT NULL DEFAULT 'pending',
last_synced_at TEXT,
sync_error TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
deleted_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_digital_memories_key ON digital_memories(key, status, updated_at);
CREATE INDEX IF NOT EXISTS idx_digital_memories_category ON digital_memories(category, status, updated_at);
CREATE INDEX IF NOT EXISTS idx_digital_memories_sync ON digital_memories(sync_status);
`;
function runStep(title, command, args, options = {}) {
console.log(`\n[DigitalEmployeeCheck] ${title}`);
const result = spawnSync(command, args, {
cwd: packageRoot,
stdio: "inherit",
shell: process.platform === "win32",
...options,
});
if (result.status !== 0) {
process.exitCode = result.status || 1;
throw new Error(`${title} failed`);
}
}
function checkSgrobotDigitalAssets() {
console.log("\n[DigitalEmployeeCheck] Verify embedded digital assets");
const indexPath = path.join(packageRoot, "public", "sgrobot-digital", "index.html");
const html = fs.readFileSync(indexPath, "utf8");
const assetRefs = [...html.matchAll(/(?:src|href)="\.\/(assets\/[^"]+)"/g)]
.map((match) => match[1]);
if (assetRefs.length === 0) {
throw new Error("No embedded digital assets referenced by index.html");
}
for (const assetRef of assetRefs) {
const assetPath = path.join(packageRoot, "public", "sgrobot-digital", assetRef);
if (!fs.existsSync(assetPath)) {
throw new Error(`Missing embedded digital asset: ${assetRef}`);
}
}
console.log(`[DigitalEmployeeCheck] ${assetRefs.length} asset references OK`);
}
function checkDigitalDiagnosticAuditProjection() {
console.log("\n[DigitalEmployeeCheck] Verify diagnostic/audit projection hooks");
const adapterPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "lib", "qimingclawAdapter.ts");
const typesPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "types", "api.ts");
const pagePath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "pages", "digital", "CommandDeck.tsx");
const adapter = fs.readFileSync(adapterPath, "utf8");
const types = fs.readFileSync(typesPath, "utf8");
const page = fs.readFileSync(pagePath, "utf8");
const requiredSnippets = [
[adapter, "'audits'", "audit business view kind"],
[adapter, "buildDiagnosticSummary", "diagnostic summary builder"],
[adapter, "filterAuditRows", "audit filters"],
[adapter, "diagnosticNotifications", "diagnostic notifications"],
[adapter, "auditNotifications", "audit notifications"],
[types, "DigitalEmployeeDiagnosticSummary", "diagnostic summary type"],
[types, "diagnostic_summary?: DigitalEmployeeDiagnosticSummary", "workday diagnostic field"],
[page, "de-diagnostic-summary-strip", "dashboard diagnostic strip"],
];
const missing = requiredSnippets
.filter(([content, snippet]) => !content.includes(snippet))
.map(([, , label]) => label);
if (missing.length > 0) {
throw new Error(`Missing diagnostic/audit projection hooks: ${missing.join(", ")}`);
}
console.log("[DigitalEmployeeCheck] diagnostic/audit projection hooks OK");
}
function checkDigitalNotificationPreferences() {
console.log("\n[DigitalEmployeeCheck] Verify notification preference hooks");
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 typesPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "types", "api.ts");
const shellPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "pages", "digital", "DigitalEmployeeShell.tsx");
const stateServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "stateService.ts");
const syncServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "syncService.ts");
const adapter = fs.readFileSync(adapterPath, "utf8");
const api = fs.readFileSync(apiPath, "utf8");
const types = fs.readFileSync(typesPath, "utf8");
const shell = fs.readFileSync(shellPath, "utf8");
const stateService = fs.readFileSync(stateServicePath, "utf8");
const syncService = fs.readFileSync(syncServicePath, "utf8");
const requiredSnippets = [
[adapter, "/api/notifications/preferences", "adapter notification preference endpoint"],
[adapter, "route-decisions|notifications|audits", "management notification business route"],
[adapter, "notificationRows", "management notification rows"],
[adapter, "filterNotificationRows", "management notification filters"],
[adapter, "muted_by_preferences", "management notification muted marker"],
[adapter, "recordNotificationTaskInput", "notification reply task input bridge"],
[adapter, "notification_reply", "notification reply task input source"],
[adapter, "task_input_recorded", "notification reply task input result"],
[adapter, "provide_task_input", "notification reply governance command"],
[adapter, "handleNotificationPreferencesPatch", "adapter notification preference patch handler"],
[adapter, "notificationMatchesPreferences", "adapter notification preference filter"],
[adapter, "update_notification_preferences", "adapter notification preference save action"],
[api, "getNotificationPreferences", "notification preferences API getter"],
[api, "patchNotificationPreferences", "notification preferences API patcher"],
[types, "NotificationPreferences", "notification preferences type"],
[shell, "de-notification-preferences", "notification preferences UI"],
[shell, "NOTIFICATION_KIND_OPTIONS", "notification kind mute UI options"],
[stateService, "update_notification_preferences", "notification preference runtime event"],
[syncService, "notificationBusinessView", "notification sync business view"],
[syncService, "governanceProvideTaskInputBusinessView", "notification reply task input sync view"],
[syncService, "governance_provide_task_input", "task input sync event kind"],
[syncService, "input_length", "task input sync redacted length"],
[syncService, "isNotificationEvent", "notification event classifier"],
[syncService, "return \"notification\"", "notification business category"],
];
const missing = requiredSnippets
.filter(([content, snippet]) => !content.includes(snippet))
.map(([, , label]) => label);
if (missing.length > 0) {
throw new Error(`Missing notification preference hooks: ${missing.join(", ")}`);
}
console.log("[DigitalEmployeeCheck] notification preference hooks OK");
}
function checkDigitalApprovalHistorySubscriptions() {
console.log("\n[DigitalEmployeeCheck] Verify approval history/subscription hooks");
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 typesPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "types", "api.ts");
const stateServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "stateService.ts");
const syncServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "syncService.ts");
const adapter = fs.readFileSync(adapterPath, "utf8");
const api = fs.readFileSync(apiPath, "utf8");
const types = fs.readFileSync(typesPath, "utf8");
const stateService = fs.readFileSync(stateServicePath, "utf8");
const syncService = fs.readFileSync(syncServicePath, "utf8");
const requiredSnippets = [
[adapter, "/api/approval/subscriptions", "adapter approval subscription endpoint"],
[adapter, "/api/digital-employee/approval-history", "adapter approval history endpoint"],
[adapter, "approvalHistoryRows", "management approval history rows"],
[adapter, "filterApprovalHistoryRows", "management approval history filters"],
[adapter, "approvalSubscriptionPreferences", "adapter approval subscription preferences"],
[adapter, "update_approval_subscriptions", "adapter approval subscription save action"],
[adapter, "approval-history", "approval history business route"],
[api, "getApprovalSubscriptionPreferences", "approval subscription API getter"],
[api, "patchApprovalSubscriptionPreferences", "approval subscription API patcher"],
[api, "getApprovalHistory", "approval history API getter"],
[types, "ApprovalSubscriptionPreferences", "approval subscription preferences type"],
[stateService, "approvalSubscriptionPreferences", "approval subscription UI state"],
[stateService, "update_approval_subscriptions", "approval subscription runtime event"],
[syncService, "approvalDecisionBusinessView", "approval decision sync business view"],
[syncService, "approvalSubscriptionBusinessView", "approval subscription sync business view"],
[syncService, "digital_workday_update_approval_subscriptions", "approval subscription sync event kind"],
];
const missing = requiredSnippets
.filter(([content, snippet]) => !content.includes(snippet))
.map(([, , label]) => label);
if (missing.length > 0) {
throw new Error(`Missing approval history/subscription hooks: ${missing.join(", ")}`);
}
console.log("[DigitalEmployeeCheck] approval history/subscription hooks OK");
}
function checkDigitalApprovalWorkflowSync() {
console.log("\n[DigitalEmployeeCheck] Verify approval workflow/sync hooks");
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 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 stateServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "stateService.ts");
const syncServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "syncService.ts");
const ipcHandlersPath = path.join(packageRoot, "src", "main", "ipc", "digitalEmployeeHandlers.ts");
const preloadPath = path.join(packageRoot, "src", "preload", "webviewPerfBridge.ts");
const adapter = fs.readFileSync(adapterPath, "utf8");
const api = fs.readFileSync(apiPath, "utf8");
const types = fs.readFileSync(typesPath, "utf8");
const commandDeck = fs.readFileSync(commandDeckPath, "utf8");
const stateService = fs.readFileSync(stateServicePath, "utf8");
const syncService = fs.readFileSync(syncServicePath, "utf8");
const ipcHandlers = fs.readFileSync(ipcHandlersPath, "utf8");
const preload = fs.readFileSync(preloadPath, "utf8");
const requiredSnippets = [
[stateService, "DigitalEmployeeApprovalWorkflow", "approval workflow type"],
[stateService, "normalizeDigitalEmployeeApprovalWorkflow", "approval workflow normalizer"],
[stateService, "applyApprovalWorkflowDecision", "approval workflow decision advancer"],
[stateService, "applyApprovalWorkflowAction", "approval workflow action updater"],
[stateService, "upsertDigitalEmployeeApprovalFromRemote", "remote approval update upsert helper"],
[stateService, "decision_history", "approval workflow decision history field"],
[syncService, "pullDigitalEmployeeApprovalUpdates", "remote approval updates pull service"],
[syncService, "submitDigitalEmployeeApprovalDecision", "approval decision submit service"],
[syncService, "/api/digital-employee/approvals/updates", "remote approval updates default path"],
[syncService, "/api/digital-employee/approvals/decisions", "remote approval decisions default path"],
[syncService, "approvalUpdatesEndpoint", "approval updates endpoint config"],
[syncService, "approvalDecisionsEndpoint", "approval decisions endpoint config"],
[syncService, "approvalSyncBusinessView", "approval sync business view"],
[syncService, "workflow_id", "approval workflow sync business field"],
[ipcHandlers, "digitalEmployee:pullApprovalUpdates", "IPC approval updates pull handler"],
[ipcHandlers, "digitalEmployee:submitApprovalDecision", "IPC approval decision submit handler"],
[preload, "pullApprovalUpdates", "preload approval updates bridge"],
[preload, "submitApprovalDecision", "preload approval decision bridge"],
[adapter, "pullApprovalUpdates", "adapter approval updates bridge declaration"],
[adapter, "submitApprovalDecision", "adapter approval decision bridge declaration"],
[adapter, "/api/approval/updates/pull", "adapter approval updates pull endpoint"],
[adapter, "approvalWorkflowSummary", "adapter approval workflow summary"],
[api, "pullApprovalUpdates", "embedded approval updates API"],
[types, "current_step_label", "approval decision current step type field"],
[types, "approval_step_count", "approval decision step count type field"],
[commandDeck, "pull_approval_updates", "home approval updates pull action"],
[commandDeck, "审批更新", "home approval updates pull button"],
];
const missing = requiredSnippets
.filter(([content, snippet]) => !content.includes(snippet))
.map(([, , label]) => label);
if (missing.length > 0) {
throw new Error(`Missing approval workflow/sync hooks: ${missing.join(", ")}`);
}
console.log("[DigitalEmployeeCheck] approval workflow/sync hooks OK");
}
function checkDigitalPlanStepGraphDispatchPolicy() {
console.log("\n[DigitalEmployeeCheck] Verify PlanStep graph/dispatch policy hooks");
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 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 schedulerServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "schedulerService.ts");
const syncServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "syncService.ts");
const ipcHandlersPath = path.join(packageRoot, "src", "main", "ipc", "digitalEmployeeHandlers.ts");
const preloadPath = path.join(packageRoot, "src", "preload", "webviewPerfBridge.ts");
const adapter = fs.readFileSync(adapterPath, "utf8");
const api = fs.readFileSync(apiPath, "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 schedulerService = fs.readFileSync(schedulerServicePath, "utf8");
const syncService = fs.readFileSync(syncServicePath, "utf8");
const ipcHandlers = fs.readFileSync(ipcHandlersPath, "utf8");
const preload = fs.readFileSync(preloadPath, "utf8");
const requiredSnippets = [
[adapter, "/api/digital-employee/plan-steps", "adapter plan step endpoint"],
[adapter, "/api/digital-employee/plan-step-graph", "adapter plan step graph endpoint"],
[adapter, "dispatch-ready-steps", "adapter plan-scoped ready step dispatch endpoint"],
[adapter, "dispatchPlanReadySteps", "adapter plan-scoped ready step dispatcher"],
[adapter, "/api/plan-step/dispatch-policy", "adapter plan step dispatch policy endpoint"],
[adapter, "/api/plan-step/dispatch-policy/pull", "adapter plan step dispatch policy pull endpoint"],
[adapter, "pullPlanStepDispatchPolicy", "adapter plan step dispatch policy pull bridge"],
[adapter, "planStepRows", "management plan step rows"],
[adapter, "planStepGraphRows", "management plan step graph rows"],
[adapter, "filterPlanStepRows", "management plan step filters"],
[adapter, "activePlanStepDispatchLease", "adapter active plan step lease detector"],
[adapter, "lease_active", "adapter lease-active readiness skip reason"],
[adapter, "lease_state", "adapter plan step lease state projection"],
[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"],
[api, "getPlanStepDispatchPolicy", "plan step dispatch policy API getter"],
[api, "patchPlanStepDispatchPolicy", "plan step dispatch policy API patcher"],
[api, "pullPlanStepDispatchPolicy", "plan step dispatch policy API puller"],
[api, "getPlanSteps", "plan steps API getter"],
[api, "getPlanStepGraph", "plan step graph API getter"],
[api, "dispatchPlanReadySteps", "plan-scoped ready step dispatch API"],
[api, "BusinessViewResponse<PlanStepBusinessRow>", "typed plan step business view API"],
[types, "trigger_source", "ready step dispatch trigger source type"],
[types, "orchestration_id", "ready step dispatch orchestration id type"],
[types, "PlanStepDispatchPolicy", "plan step dispatch policy type"],
[types, "PlanStepDispatchPolicyPullResult", "plan step dispatch policy pull result type"],
[types, "last_pull_error", "plan step dispatch policy pull error field"],
[types, "PlanStepLeaseSummary", "plan step lease summary type"],
[types, "remote_lease_id", "plan step remote lease type field"],
[types, "PlanStepDependencyDetail", "plan step blocked dependency detail type"],
[types, "lease_active", "plan step lease active type field"],
[types, "PlanStepBusinessRow", "plan step business row type"],
[types, "BusinessViewResponse", "business view response type"],
[commandDeck, "getPlanStepGraph", "home blocked plan step graph fetch"],
[commandDeck, "planStepOrchestrationSummary", "home plan step orchestration summary"],
[commandDeck, "leasedStepCount", "home plan step lease count"],
[commandDeck, "controlPlanReadySteps", "home plan-scoped ready step control"],
[commandDeck, "pull_plan_step_dispatch_policy", "home plan step dispatch policy pull action"],
[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, "update_plan_step_dispatch_policy", "plan step dispatch policy runtime event"],
[stateService, "pull_plan_step_dispatch_policy", "plan step dispatch policy pull runtime event"],
[stateService, "last_pulled_at", "plan step dispatch policy last pulled field"],
[stateService, "last_pull_error", "plan step dispatch policy last pull error field"],
[stateService, "planIdFilter", "plan-scoped ready step candidate filter"],
[stateService, "dispatchLease", "plan step payload lease state"],
[stateService, "activeDigitalEmployeePlanStepLease", "plan step active lease helper"],
[stateService, "acquireDigitalEmployeePlanStepLease", "plan step lease acquire helper"],
[stateService, "releaseDigitalEmployeePlanStepLease", "plan step lease release helper"],
[stateService, "recordDigitalEmployeePlanStepLeaseSkipped", "plan step remote lease rejected helper"],
[stateService, "local_fallback", "plan step local fallback lease source"],
[stateService, "remote_release_error", "plan step remote release error state"],
[stateService, "plan_step_lease_acquired", "plan step lease acquired event"],
[stateService, "plan_step_lease_released", "plan step lease released event"],
[stateService, "plan_step_lease_skipped", "plan step lease skipped event"],
[schedulerService, "bypassAutoDispatchSwitch", "manual plan dispatch auto switch bypass"],
[schedulerService, "runDigitalEmployeePlanReadyStepsNow", "manual plan-scoped ready step dispatcher"],
[schedulerService, "acquireDigitalEmployeePlanStepLease", "scheduler acquire plan step lease"],
[schedulerService, "releaseDigitalEmployeePlanStepLease", "scheduler release plan step lease"],
[schedulerService, "acquireDigitalEmployeePlanStepRemoteLease", "scheduler remote plan step lease acquire"],
[schedulerService, "releaseDigitalEmployeePlanStepRemoteLease", "scheduler remote plan step lease release"],
[schedulerService, "remote_lease_rejected", "scheduler remote lease rejected skip reason"],
[schedulerService, "local_fallback", "scheduler remote lease fallback source"],
[schedulerService, "triggerSource", "ready step dispatch trigger source"],
[schedulerService, "readDigitalEmployeePlanStepDispatchPolicy", "scheduler plan step dispatch policy reader"],
[schedulerService, "pullDigitalEmployeePlanStepDispatchPolicy", "scheduler remote plan step dispatch policy pull"],
[schedulerService, "max_per_sweep", "scheduler max per sweep policy"],
[ipcHandlers, "digitalEmployee:dispatchPlanReadySteps", "IPC plan-scoped ready step dispatch handler"],
[ipcHandlers, "digitalEmployee:pullPlanStepDispatchPolicy", "IPC plan step dispatch policy pull handler"],
[preload, "dispatchPlanReadySteps", "preload plan-scoped ready step bridge"],
[preload, "pullPlanStepDispatchPolicy", "preload plan step dispatch policy pull bridge"],
[syncService, "planStepBusinessView", "plan step sync business view"],
[syncService, "pullDigitalEmployeePlanStepDispatchPolicy", "remote plan step dispatch policy pull service"],
[syncService, "planStepDispatchPolicyEndpoint", "remote plan step dispatch policy endpoint config"],
[syncService, "/api/digital-employee/policies/plan-step-dispatch", "remote plan step dispatch policy default path"],
[syncService, "acquireDigitalEmployeePlanStepRemoteLease", "remote plan step dispatch lease acquire service"],
[syncService, "releaseDigitalEmployeePlanStepRemoteLease", "remote plan step dispatch lease release service"],
[syncService, "planStepDispatchLeaseEndpoint", "remote plan step dispatch lease endpoint config"],
[syncService, "/api/digital-employee/leases/plan-step-dispatch", "remote plan step dispatch lease default path"],
[syncService, "lease_active", "plan step sync lease active field"],
[syncService, "remote_lease_error", "plan step sync remote lease error field"],
[syncService, "remote_release_error", "plan step sync remote release error field"],
[syncService, "planStepLeaseBusinessView", "plan step lease sync event view"],
[syncService, "plan_step_lease_acquired", "plan step lease acquired sync event kind"],
[syncService, "trigger_source", "plan step dispatch sync trigger source"],
[syncService, "orchestration_id", "plan step dispatch sync orchestration id"],
[syncService, "planStepDispatchPolicyBusinessView", "plan step dispatch policy sync view"],
[syncService, "digital_workday_update_plan_step_dispatch_policy", "plan step dispatch policy sync event kind"],
[syncService, "digital_workday_pull_plan_step_dispatch_policy", "plan step dispatch policy pull sync event kind"],
];
const missing = requiredSnippets
.filter(([content, snippet]) => !content.includes(snippet))
.map(([, , label]) => label);
if (missing.length > 0) {
throw new Error(`Missing PlanStep graph/dispatch policy hooks: ${missing.join(", ")}`);
}
console.log("[DigitalEmployeeCheck] PlanStep graph/dispatch policy hooks OK");
}
function checkDigitalSkillRemotePolicy() {
console.log("\n[DigitalEmployeeCheck] Verify skill remote policy hooks");
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 skillLibraryPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "pages", "digital", "SkillLibrary.tsx");
const skillExecutionPolicyPath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "skillExecutionPolicy.ts");
const skillCatalogPath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "skillCatalogService.ts");
const syncServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "syncService.ts");
const ipcHandlersPath = path.join(packageRoot, "src", "main", "ipc", "digitalEmployeeHandlers.ts");
const preloadPath = path.join(packageRoot, "src", "preload", "webviewPerfBridge.ts");
const adapter = fs.readFileSync(adapterPath, "utf8");
const api = fs.readFileSync(apiPath, "utf8");
const skillLibrary = fs.readFileSync(skillLibraryPath, "utf8");
const skillExecutionPolicy = fs.readFileSync(skillExecutionPolicyPath, "utf8");
const skillCatalog = fs.readFileSync(skillCatalogPath, "utf8");
const syncService = fs.readFileSync(syncServicePath, "utf8");
const ipcHandlers = fs.readFileSync(ipcHandlersPath, "utf8");
const preload = fs.readFileSync(preloadPath, "utf8");
const requiredSnippets = [
[skillExecutionPolicy, "local_skills", "local skill policy map"],
[skillExecutionPolicy, "remote_skills", "remote skill policy map"],
[skillExecutionPolicy, "last_pull_error", "skill policy pull error metadata"],
[skillExecutionPolicy, "effectiveSkillPolicyFor", "effective skill policy merge helper"],
[skillExecutionPolicy, "matched_remote_policy", "skill call remote policy audit snapshot"],
[skillCatalog, "remote_policy", "skill catalog remote policy projection"],
[skillCatalog, "local_policy", "skill catalog local policy projection"],
[skillCatalog, "policy_source", "skill catalog policy source projection"],
[syncService, "pullDigitalEmployeeSkillPolicies", "remote skill policy pull service"],
[syncService, "skillPolicyEndpoint", "remote skill policy endpoint config"],
[syncService, "/api/digital-employee/policies/skills", "remote skill policy default path"],
[syncService, "readRemoteSkillPolicyPayload", "remote skill policy response parser"],
[syncService, "recordSkillPolicyPullFailure", "remote skill policy pull failure recorder"],
[ipcHandlers, "digitalEmployee:pullSkillPolicies", "IPC skill policy pull handler"],
[preload, "pullSkillPolicies", "preload skill policy pull bridge"],
[adapter, "pullSkillPolicies", "adapter skill policy pull bridge"],
[adapter, "/api/skill/policies/pull", "adapter skill policy pull endpoint"],
[api, "pullSkillPolicies", "embedded skill policy pull API"],
[skillLibrary, "拉取策略", "skill library pull policy action"],
[skillLibrary, "远端策略", "skill library remote policy marker"],
];
const missing = requiredSnippets
.filter(([content, snippet]) => !content.includes(snippet))
.map(([, , label]) => label);
if (missing.length > 0) {
throw new Error(`Missing skill remote policy hooks: ${missing.join(", ")}`);
}
console.log("[DigitalEmployeeCheck] skill remote policy hooks OK");
}
function checkDigitalRiskApprovalPolicy() {
console.log("\n[DigitalEmployeeCheck] Verify risk approval policy hooks");
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 commandDeckPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "pages", "digital", "CommandDeck.tsx");
const stateServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "stateService.ts");
const syncServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "syncService.ts");
const ipcHandlersPath = path.join(packageRoot, "src", "main", "ipc", "digitalEmployeeHandlers.ts");
const preloadPath = path.join(packageRoot, "src", "preload", "webviewPerfBridge.ts");
const adapter = fs.readFileSync(adapterPath, "utf8");
const api = fs.readFileSync(apiPath, "utf8");
const commandDeck = fs.readFileSync(commandDeckPath, "utf8");
const stateService = fs.readFileSync(stateServicePath, "utf8");
const syncService = fs.readFileSync(syncServicePath, "utf8");
const ipcHandlers = fs.readFileSync(ipcHandlersPath, "utf8");
const preload = fs.readFileSync(preloadPath, "utf8");
const requiredSnippets = [
[stateService, "riskApprovalPolicy", "risk approval policy UI state"],
[stateService, "normalizeRiskApprovalPolicy", "risk approval policy normalizer"],
[stateService, "evaluateDigitalEmployeeRiskPolicy", "risk approval policy evaluator"],
[stateService, "risk_approval_required", "risk approval-required event kind"],
[stateService, "risk_policy_rejected", "risk policy rejected event kind"],
[stateService, "pull_risk_approval_policy", "risk approval policy pull UI event"],
[syncService, "pullDigitalEmployeeRiskApprovalPolicy", "remote risk approval policy pull service"],
[syncService, "riskApprovalPolicyEndpoint", "remote risk approval policy endpoint config"],
[syncService, "/api/digital-employee/policies/risk-approval", "remote risk approval policy default path"],
[syncService, "risk_approval_required", "risk approval-required sync event kind"],
[syncService, "risk_policy_rejected", "risk policy rejected sync event kind"],
[syncService, "riskPolicyBusinessView", "risk policy sync business view"],
[ipcHandlers, "digitalEmployee:pullRiskApprovalPolicy", "IPC risk approval policy pull handler"],
[ipcHandlers, "digitalEmployee:evaluateRiskPolicy", "IPC risk policy evaluation handler"],
[preload, "pullRiskApprovalPolicy", "preload risk approval policy pull bridge"],
[preload, "evaluateRiskPolicy", "preload risk policy evaluation bridge"],
[adapter, "pullRiskApprovalPolicy", "adapter risk approval policy pull bridge"],
[adapter, "evaluateRiskPolicy", "adapter risk policy evaluation bridge"],
[adapter, "/api/risk-approval/policy/pull", "adapter risk approval policy pull endpoint"],
[adapter, "riskPolicyBlockedResponse", "adapter risk policy blocked response"],
[adapter, "governanceCommandRiskLevel", "adapter governance risk level mapper"],
[adapter, "managed_service.restart", "managed service restart risk action"],
[adapter, "artifact.access", "artifact access risk action"],
[adapter, "plan_step.dispatch_ready_steps", "ready PlanStep dispatch risk action"],
[api, "pullRiskApprovalPolicy", "embedded risk approval policy pull API"],
[commandDeck, "pull_risk_approval_policy", "home risk approval policy pull action"],
[commandDeck, "风险策略", "home risk approval policy pull button"],
];
const missing = requiredSnippets
.filter(([content, snippet]) => !content.includes(snippet))
.map(([, , label]) => label);
if (missing.length > 0) {
throw new Error(`Missing risk approval policy hooks: ${missing.join(", ")}`);
}
console.log("[DigitalEmployeeCheck] risk approval policy hooks OK");
}
function checkDigitalRouteDecisionPolicy() {
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 commandDeckPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "pages", "digital", "CommandDeck.tsx");
const typesPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "types", "api.ts");
const stateServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "stateService.ts");
const syncServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "syncService.ts");
const ipcHandlersPath = path.join(packageRoot, "src", "main", "ipc", "digitalEmployeeHandlers.ts");
const preloadPath = path.join(packageRoot, "src", "preload", "webviewPerfBridge.ts");
const adapter = fs.readFileSync(adapterPath, "utf8");
const api = fs.readFileSync(apiPath, "utf8");
const commandDeck = fs.readFileSync(commandDeckPath, "utf8");
const types = fs.readFileSync(typesPath, "utf8");
const stateService = fs.readFileSync(stateServicePath, "utf8");
const syncService = fs.readFileSync(syncServicePath, "utf8");
const ipcHandlers = fs.readFileSync(ipcHandlersPath, "utf8");
const preload = fs.readFileSync(preloadPath, "utf8");
const requiredSnippets = [
[stateService, "routeDecisionPolicy", "route decision policy UI state"],
[stateService, "normalizeRouteDecisionPolicy", "route decision policy normalizer"],
[stateService, "evaluateDigitalEmployeeRouteDecisionPolicy", "route decision policy evaluator"],
[stateService, "listDigitalEmployeeRouteInterventions", "route intervention list helper"],
[stateService, "recordDigitalEmployeeRouteInterventionResolution", "route intervention resolution recorder"],
[stateService, "route_decision_blocked", "route decision blocked event kind"],
[stateService, "route_decision_approval_required", "route decision approval-required event kind"],
[stateService, "route_decision_intervention_resolved", "route decision intervention resolved event kind"],
[syncService, "pullDigitalEmployeeRouteDecisionPolicy", "remote route decision policy pull service"],
[syncService, "routeDecisionPolicyEndpoint", "remote route decision policy endpoint config"],
[syncService, "/api/digital-employee/policies/route-decision", "remote route decision policy default path"],
[syncService, "routeDecisionPolicyBusinessView", "route decision policy sync business view"],
[syncService, "routeDecisionInterventionBusinessView", "route decision intervention sync business view"],
[ipcHandlers, "digitalEmployee:pullRouteDecisionPolicy", "IPC route decision policy pull handler"],
[ipcHandlers, "digitalEmployee:evaluateRouteDecisionPolicy", "IPC route decision policy evaluation handler"],
[ipcHandlers, "digitalEmployee:listRouteInterventions", "IPC route intervention list handler"],
[ipcHandlers, "digitalEmployee:recordRouteInterventionResolution", "IPC route intervention resolution handler"],
[preload, "pullRouteDecisionPolicy", "preload route decision policy pull bridge"],
[preload, "evaluateRouteDecisionPolicy", "preload route decision policy evaluation bridge"],
[preload, "listRouteInterventions", "preload route intervention list bridge"],
[preload, "recordRouteInterventionResolution", "preload route intervention resolution bridge"],
[adapter, "pullRouteDecisionPolicy", "adapter route decision policy pull bridge"],
[adapter, "evaluateRouteDecisionPolicy", "adapter route decision policy evaluation bridge"],
[adapter, "/api/route-decision/policy/pull", "adapter route decision policy pull endpoint"],
[adapter, "/api/route-decisions/interventions", "adapter route intervention list endpoint"],
[adapter, "handleRouteInterventionDecision", "adapter route intervention decision handler"],
[adapter, "resolveApprovedRouteInterventions", "adapter approved route intervention auto resolver"],
[adapter, "auto_create_governance_commands", "adapter route command auto-create switch"],
[api, "pullRouteDecisionPolicy", "embedded route decision policy pull API"],
[api, "decideRouteIntervention", "embedded route intervention decision API"],
[types, "RouteDecisionPolicy", "embedded route decision policy type"],
[types, "RouteInterventionRecord", "embedded route intervention type"],
[commandDeck, "pull_route_decision_policy", "home route decision policy pull action"],
[commandDeck, "路由策略", "home route decision policy pull button"],
];
const missing = requiredSnippets
.filter(([content, snippet]) => !content.includes(snippet))
.map(([, , label]) => label);
if (missing.length > 0) {
throw new Error(`Missing route decision policy hooks: ${missing.join(", ")}`);
}
console.log("[DigitalEmployeeCheck] route decision policy hooks OK");
}
function checkLocalDatabaseSchema() {
console.log("\n[DigitalEmployeeCheck] Verify local SQLite digital schema");
const dbPath = path.join(os.homedir(), ".qimingclaw", "qimingclaw.db");
if (!fs.existsSync(dbPath)) {
console.log(`[DigitalEmployeeCheck] Skip DB schema check; database not found: ${dbPath}`);
return;
}
const healResult = spawnSync("sqlite3", [dbPath, selfHealingDigitalSchemaSql], {
encoding: "utf8",
shell: process.platform === "win32",
});
if (healResult.error) {
console.log(`[DigitalEmployeeCheck] Skip DB schema check; sqlite3 unavailable: ${healResult.error.message}`);
return;
}
if (healResult.status !== 0) {
throw new Error(healResult.stderr || "sqlite3 digital schema self-heal failed");
}
const result = spawnSync("sqlite3", [dbPath, ".tables"], {
encoding: "utf8",
shell: process.platform === "win32",
});
if (result.status !== 0) {
throw new Error(result.stderr || "sqlite3 .tables failed");
}
const tables = new Set(result.stdout.split(/\s+/).filter(Boolean));
const missing = requiredDigitalTables.filter((table) => !tables.has(table));
if (missing.length > 0) {
throw new Error(`Missing digital employee tables: ${missing.join(", ")}`);
}
console.log(`[DigitalEmployeeCheck] ${requiredDigitalTables.length} digital tables OK`);
}
function main() {
try {
runStep("Run digital employee unit tests", "npm", ["run", "test:run", "--", ...testFiles]);
runStep("Build Electron main process", "npm", ["run", "build:main:dev"]);
runStep("Build embedded digital employee", "npm", ["run", "build:sgrobot-digital"]);
checkSgrobotDigitalAssets();
checkDigitalDiagnosticAuditProjection();
checkDigitalNotificationPreferences();
checkDigitalApprovalHistorySubscriptions();
checkDigitalApprovalWorkflowSync();
checkDigitalPlanStepGraphDispatchPolicy();
checkDigitalSkillRemotePolicy();
checkDigitalRiskApprovalPolicy();
checkDigitalRouteDecisionPolicy();
checkLocalDatabaseSchema();
console.log("\n[DigitalEmployeeCheck] All checks passed");
} catch (error) {
console.error(`\n[DigitalEmployeeCheck] ${error instanceof Error ? error.message : String(error)}`);
process.exit(process.exitCode || 1);
}
}
main();