Files
qiming/qimingclaw/crates/agent-electron-client/scripts/check-digital-employee.js
2026-06-10 21:00:59 +08:00

419 lines
22 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 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, "planStepRows", "management plan step rows"],
[adapter, "planStepGraphRows", "management plan step graph rows"],
[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"],
[api, "getPlanStepDispatchPolicy", "plan step dispatch policy API getter"],
[api, "patchPlanStepDispatchPolicy", "plan step dispatch policy API patcher"],
[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, "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, "planStepOrchestrationSummary", "home plan step orchestration summary"],
[commandDeck, "controlPlanReadySteps", "home plan-scoped ready step control"],
[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, "planIdFilter", "plan-scoped ready step candidate filter"],
[schedulerService, "bypassAutoDispatchSwitch", "manual plan dispatch auto switch bypass"],
[schedulerService, "runDigitalEmployeePlanReadyStepsNow", "manual plan-scoped ready step dispatcher"],
[schedulerService, "triggerSource", "ready step dispatch trigger source"],
[schedulerService, "readDigitalEmployeePlanStepDispatchPolicy", "scheduler plan step dispatch policy reader"],
[schedulerService, "max_per_sweep", "scheduler max per sweep policy"],
[ipcHandlers, "digitalEmployee:dispatchPlanReadySteps", "IPC plan-scoped ready step dispatch handler"],
[preload, "dispatchPlanReadySteps", "preload plan-scoped ready step bridge"],
[syncService, "planStepBusinessView", "plan step sync business view"],
[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"],
];
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 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();
checkDigitalPlanStepGraphDispatchPolicy();
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();