Files
qiming/qimingclaw/crates/agent-electron-client/src/main/services/digitalEmployee/stateService.ts
2026-06-09 09:34:39 +08:00

1718 lines
50 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ensureDigitalEmployeeSchema, getDb, readSetting, writeSetting } from "../../db";
const STATE_KEY = "digital_employee_state_v1";
const UI_STATE_KEY = "digital_employee_ui_state_v1";
const MAX_EVENTS = 200;
export interface DigitalEmployeeServiceStatus {
running?: boolean;
error?: string;
engineType?: string | null;
port?: number;
serverCount?: number;
serverNames?: string[];
pid?: number;
}
export interface DigitalEmployeeSessionSummary {
total: number;
active: number;
items: Array<{
id: string;
title?: string;
status?: string;
engineType?: string;
projectId?: string;
}>;
}
export interface DigitalEmployeeManagedService {
serviceId: string;
name: string;
kind: "agent" | "mcp" | "desktop" | "network" | "file" | "gui" | "unknown";
status: "running" | "stopped" | "degraded";
statusLabel: string;
running: boolean;
error?: string | null;
requiresHumanAction: boolean;
dependentTasks: string[];
restartCount: number;
lastSeenAt: string;
instance: {
pid?: number;
port?: number;
engineType?: string | null;
serverCount?: number;
serverNames?: string[];
};
lease: {
state: "active" | "stopped" | "degraded";
updatedAt: string;
};
}
export interface DigitalEmployeeSnapshot {
generatedAt: string;
services: Record<string, DigitalEmployeeServiceStatus | null>;
managedServices?: DigitalEmployeeManagedService[];
sessions: DigitalEmployeeSessionSummary;
}
export interface DigitalEmployeePlanRecord {
id: string;
remoteId?: string | null;
title: string;
objective?: string | null;
status: string;
source: string;
payload: unknown;
syncStatus: string;
lastSyncedAt?: string | null;
syncError?: string | null;
createdAt: string;
updatedAt: string;
}
export interface DigitalEmployeeTaskRecord {
id: string;
remoteId?: string | null;
planId?: string | null;
title: string;
status: string;
assignedSkillId?: string | null;
payload: unknown;
syncStatus: string;
lastSyncedAt?: string | null;
syncError?: string | null;
createdAt: string;
updatedAt: string;
}
export interface DigitalEmployeeRunRecord {
id: string;
remoteId?: string | null;
planId?: string | null;
taskId?: string | null;
status: string;
startedAt?: string | null;
finishedAt?: string | null;
payload: unknown;
syncStatus: string;
lastSyncedAt?: string | null;
syncError?: string | null;
createdAt: string;
updatedAt: string;
}
export interface DigitalEmployeeEventRecord {
id: string;
remoteId?: string | null;
planId?: string | null;
taskId?: string | null;
runId?: string | null;
kind: string;
message: string;
payload: unknown;
syncStatus: string;
lastSyncedAt?: string | null;
syncError?: string | null;
occurredAt: string;
createdAt: string;
}
export interface DigitalEmployeeArtifactRecord {
id: string;
remoteId?: string | null;
planId?: string | null;
taskId?: string | null;
runId?: string | null;
kind: string;
name?: string | null;
uri?: string | null;
payload: unknown;
syncStatus: string;
lastSyncedAt?: string | null;
syncError?: string | null;
createdAt: string;
}
export interface DigitalEmployeeApprovalRecord {
id: string;
remoteId?: string | null;
planId?: string | null;
taskId?: string | null;
runId?: string | null;
title: string;
status: string;
payload: unknown;
syncStatus: string;
lastSyncedAt?: string | null;
syncError?: string | null;
createdAt: string;
updatedAt: string;
}
export interface DigitalEmployeeRuntimeRecords {
generatedAt: string;
plans: DigitalEmployeePlanRecord[];
tasks: DigitalEmployeeTaskRecord[];
runs: DigitalEmployeeRunRecord[];
events: DigitalEmployeeEventRecord[];
artifacts: DigitalEmployeeArtifactRecord[];
approvals: DigitalEmployeeApprovalRecord[];
}
export interface DigitalEmployeeUiState {
selectedDutyIdsByDate: Record<string, string[]>;
confirmedDates: Record<string, string>;
reportScheduleTime: string;
reportGeneratedAtByDate: Record<string, string>;
decisionResultsByDate: Record<string, Record<string, string>>;
consoleRole?: string;
rolePreferences?: Record<string, unknown>;
profile: {
name: string;
company: string;
position: string;
currentStatus: string;
};
}
export interface DigitalEmployeeUiStateUpdate {
state: DigitalEmployeeUiState;
action?: string;
date?: string;
metadata?: Record<string, unknown>;
}
export interface DigitalEmployeeStateEvent {
event_id: string;
kind: string;
occurred_at: string;
message: string;
plan_id: string | null;
task_id: string | null;
}
export interface DigitalEmployeeState {
version: 1;
updatedAt: string | null;
lastSnapshot: DigitalEmployeeSnapshot | null;
events: DigitalEmployeeStateEvent[];
pendingSyncCount?: number;
}
export interface DigitalEmployeeChatRequestRecord {
user_id?: string;
project_id?: string;
prompt?: string;
session_id?: string;
request_id?: string;
model_provider?: { provider?: string; model?: string; default_model?: string };
}
export interface DigitalEmployeeChatResultRecord {
success: boolean;
code?: string;
message?: string;
project_id?: string;
session_id?: string;
request_id?: string;
engine?: string;
}
export interface DigitalEmployeeRuntimeEventRecord {
kind: string;
message?: string;
request_id?: string;
session_id?: string;
project_id?: string;
status?: "running" | "completed" | "failed" | "pending";
payload?: unknown;
}
export interface DigitalEmployeeGovernanceCommandRecord {
commandKind: string;
commandId: string;
correlationId?: string;
planId?: string | null;
scheduleId?: string | null;
accepted?: boolean;
message?: string;
error?: string | null;
payload?: Record<string, unknown> | null;
result?: Record<string, unknown> | null;
}
function defaultState(): DigitalEmployeeState {
return {
version: 1,
updatedAt: null,
lastSnapshot: null,
events: [],
};
}
function defaultUiState(): DigitalEmployeeUiState {
return {
selectedDutyIdsByDate: {},
confirmedDates: {},
reportScheduleTime: "18:00",
reportGeneratedAtByDate: {},
decisionResultsByDate: {},
consoleRole: "sales",
rolePreferences: {},
profile: {
name: "飞天数字员工",
company: "qimingclaw 客户端",
position: "客户端任务执行员",
currentStatus: "working",
},
};
}
function getDigitalEmployeeDb(): ReturnType<typeof getDb> {
if (!ensureDigitalEmployeeSchema()) return null;
return getDb();
}
function normalizeStringMap(value: unknown): Record<string, string> {
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
return Object.fromEntries(
Object.entries(value as Record<string, unknown>)
.filter((entry): entry is [string, string] =>
typeof entry[0] === "string" && typeof entry[1] === "string",
),
);
}
function normalizeStringArrayMap(value: unknown): Record<string, string[]> {
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
return Object.fromEntries(
Object.entries(value as Record<string, unknown>).map(([key, list]) => [
key,
Array.isArray(list)
? list.filter((item): item is string => typeof item === "string")
: [],
]),
);
}
function normalizeNestedStringMap(value: unknown): Record<string, Record<string, string>> {
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
return Object.fromEntries(
Object.entries(value as Record<string, unknown>).map(([key, map]) => [
key,
normalizeStringMap(map),
]),
);
}
function normalizeRecord(value: unknown): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
return { ...value as Record<string, unknown> };
}
function normalizeUiState(
value: Partial<DigitalEmployeeUiState>,
): DigitalEmployeeUiState {
const fallback = defaultUiState();
const profile: Partial<DigitalEmployeeUiState["profile"]> = value.profile || {};
return {
selectedDutyIdsByDate: normalizeStringArrayMap(value.selectedDutyIdsByDate),
confirmedDates: normalizeStringMap(value.confirmedDates),
reportScheduleTime: typeof value.reportScheduleTime === "string" && value.reportScheduleTime.trim()
? value.reportScheduleTime
: fallback.reportScheduleTime,
reportGeneratedAtByDate: normalizeStringMap(value.reportGeneratedAtByDate),
decisionResultsByDate: normalizeNestedStringMap(value.decisionResultsByDate),
consoleRole: typeof value.consoleRole === "string" && value.consoleRole.trim()
? value.consoleRole
: fallback.consoleRole,
rolePreferences: normalizeRecord(value.rolePreferences),
profile: {
name: typeof profile.name === "string" && profile.name.trim()
? profile.name
: fallback.profile.name,
company: typeof profile.company === "string" && profile.company.trim()
? profile.company
: fallback.profile.company,
position: typeof profile.position === "string" && profile.position.trim()
? profile.position
: fallback.profile.position,
currentStatus: typeof profile.currentStatus === "string" && profile.currentStatus.trim()
? profile.currentStatus
: fallback.profile.currentStatus,
},
};
}
function digitalEmployeeUiActionMessage(action: string, date?: string): string {
const suffix = date ? `${date}` : "";
switch (action) {
case "confirm_workday":
return `数字员工今日任务已确认${suffix}`;
case "update_profile":
return "数字员工资料已更新";
case "update_report_schedule":
return `数字员工日报时间已更新${suffix}`;
case "generate_daily_report":
return `数字员工日报已生成${suffix}`;
case "decide_approval":
return `数字员工待决策事项已处理${suffix}`;
case "update_console_role":
return "数字员工角色已更新";
case "update_role_preferences":
return "数字员工运营偏好已更新";
default:
return `数字员工页面状态已更新${suffix}`;
}
}
export function readDigitalEmployeeState(): DigitalEmployeeState {
const state = readSetting(STATE_KEY);
const pendingSyncCount = countPendingSyncItems();
if (!state || typeof state !== "object") {
return { ...defaultState(), pendingSyncCount };
}
return {
...defaultState(),
...(state as Partial<DigitalEmployeeState>),
version: 1,
pendingSyncCount,
};
}
export function writeDigitalEmployeeState(state: DigitalEmployeeState): void {
writeSetting(STATE_KEY, state);
}
export function readDigitalEmployeeUiState(): DigitalEmployeeUiState {
const state = readSetting(UI_STATE_KEY);
if (!state || typeof state !== "object") return defaultUiState();
return normalizeUiState(state as Partial<DigitalEmployeeUiState>);
}
export function saveDigitalEmployeeUiState(
update: DigitalEmployeeUiStateUpdate,
): DigitalEmployeeUiState {
const state = normalizeUiState(update.state);
writeSetting(UI_STATE_KEY, state);
recordUiApprovalDecision(update);
if (update.action) {
recordDigitalEmployeeRuntimeEvent({
kind: `digital_workday_${update.action}`,
message: digitalEmployeeUiActionMessage(update.action, update.date),
status: "completed",
payload: {
action: update.action,
date: update.date,
metadata: update.metadata,
state,
},
});
}
return state;
}
function recordUiApprovalDecision(update: DigitalEmployeeUiStateUpdate): void {
if (update.action !== "decide_approval") return;
const metadata = objectRecord(update.metadata);
const decisionId = stringValue(metadata?.decision_id);
const decision = stringValue(metadata?.decision);
if (!decisionId || !decision) return;
const db = getDigitalEmployeeDb();
if (!db) return;
const existing = db.prepare(`
SELECT id, plan_id, task_id, run_id, title, status, payload, created_at
FROM digital_approvals
WHERE id = ?
`).get(decisionId) as Record<string, unknown> | undefined;
if (!existing) return;
const now = new Date().toISOString();
const previousStatus = stringField(existing, "status") || "pending";
const storedPayload = parseStoredPayload(existing["payload"]);
const storedPayloadRecord = objectRecord(storedPayload);
const acpPermission = objectRecord(metadata?.acp_permission);
const payload = storedPayloadRecord
? {
...storedPayloadRecord,
decision,
decidedAt: now,
previousStatus,
...(acpPermission ? { acpPermission } : {}),
}
: {
value: storedPayload,
decision,
decidedAt: now,
previousStatus,
...(acpPermission ? { acpPermission } : {}),
};
upsertApproval({
id: decisionId,
planId: stringField(existing, "plan_id"),
taskId: stringField(existing, "task_id"),
runId: stringField(existing, "run_id"),
title: stringField(existing, "title") || "待确认事项",
status: approvalDecisionStatus(decision),
payload,
now,
});
}
function approvalDecisionStatus(decision: string): string {
const normalized = decision.toLowerCase();
if (["approve", "approved", "accept", "accepted"].includes(normalized)) return "approved";
if (["reject", "rejected", "deny", "denied"].includes(normalized)) return "rejected";
if (["dismiss", "dismissed", "skip", "skipped"].includes(normalized)) return "dismissed";
return "completed";
}
export function readDigitalEmployeeRuntimeRecords(
limit = 120,
): DigitalEmployeeRuntimeRecords {
const db = getDigitalEmployeeDb();
if (!db) {
return {
generatedAt: new Date().toISOString(),
plans: [],
tasks: [],
runs: [],
events: [],
artifacts: [],
approvals: [],
};
}
const safeLimit = Math.max(1, Math.min(Math.floor(limit), 300));
const plans = db.prepare(`
SELECT id, remote_id, title, objective, status, source, payload, sync_status,
last_synced_at, sync_error, created_at, updated_at
FROM digital_plans
ORDER BY updated_at DESC, created_at DESC
LIMIT ?
`).all(safeLimit) as Array<Record<string, unknown>>;
const tasks = db.prepare(`
SELECT id, remote_id, plan_id, title, status, assigned_skill_id, payload,
sync_status, last_synced_at, sync_error, created_at, updated_at
FROM digital_tasks
ORDER BY updated_at DESC, created_at DESC
LIMIT ?
`).all(safeLimit) as Array<Record<string, unknown>>;
const runs = db.prepare(`
SELECT id, remote_id, plan_id, task_id, status, started_at, finished_at,
payload, sync_status, last_synced_at, sync_error, created_at, updated_at
FROM digital_runs
ORDER BY updated_at DESC, created_at DESC
LIMIT ?
`).all(safeLimit) as Array<Record<string, unknown>>;
const events = db.prepare(`
SELECT id, remote_id, plan_id, task_id, run_id, kind, message, payload,
sync_status, last_synced_at, sync_error, occurred_at, created_at
FROM digital_events
ORDER BY occurred_at DESC, created_at DESC
LIMIT ?
`).all(safeLimit) as Array<Record<string, unknown>>;
const artifacts = db.prepare(`
SELECT id, remote_id, plan_id, task_id, run_id, kind, name, uri, payload,
sync_status, last_synced_at, sync_error, created_at
FROM digital_artifacts
ORDER BY created_at DESC
LIMIT ?
`).all(safeLimit) as Array<Record<string, unknown>>;
const approvals = db.prepare(`
SELECT id, remote_id, plan_id, task_id, run_id, title, status, payload,
sync_status, last_synced_at, sync_error, created_at, updated_at
FROM digital_approvals
ORDER BY updated_at DESC, created_at DESC
LIMIT ?
`).all(safeLimit) as Array<Record<string, unknown>>;
return {
generatedAt: new Date().toISOString(),
plans: plans.map(toPlanRecord),
tasks: tasks.map(toTaskRecord),
runs: runs.map(toRunRecord),
events: events.map(toEventRecord),
artifacts: artifacts.map(toArtifactRecord),
approvals: approvals.map(toApprovalRecord),
};
}
export function recordDigitalEmployeeSnapshot(
snapshot: DigitalEmployeeSnapshot,
): DigitalEmployeeState {
const previous = readDigitalEmployeeState();
const events = [...previous.events];
const event = eventFromSnapshot(snapshot, previous.lastSnapshot);
if (event) {
events.unshift(event);
}
const next: DigitalEmployeeState = {
version: 1,
updatedAt: snapshot.generatedAt,
lastSnapshot: snapshot,
events: events.slice(0, MAX_EVENTS),
};
writeDigitalEmployeeState(next);
persistSnapshotTables(snapshot, event);
return next;
}
export function recordDigitalEmployeeChatReceived(
request: DigitalEmployeeChatRequestRecord,
): { planId: string; taskId: string; runId: string } {
const now = new Date().toISOString();
const ids = chatIds(request);
const title = chatTitle(request);
const db = getDigitalEmployeeDb();
if (!db) return ids;
const tx = db.transaction(() => {
upsertPlan({
id: ids.planId,
title: `远程任务:${title}`,
objective: request.prompt || request.project_id || "管理端下发任务",
status: "running",
payload: request,
now,
});
upsertTask({
id: ids.taskId,
planId: ids.planId,
title,
status: "running",
assignedSkillId: "qimingclaw-computer-control",
payload: request,
now,
});
upsertRun({
id: ids.runId,
planId: ids.planId,
taskId: ids.taskId,
status: "running",
payload: { request },
now,
});
persistPayloadArtifactsAndApprovals(ids, request, now);
insertEvent(
{
event_id: `${ids.runId}:received`,
kind: "computer_chat_received",
occurred_at: now,
message: `收到管理端任务:${title}`,
plan_id: ids.planId,
task_id: ids.taskId,
},
ids.runId,
{ request },
);
});
tx();
return ids;
}
export function recordDigitalEmployeeChatResult(
request: DigitalEmployeeChatRequestRecord,
result: DigitalEmployeeChatResultRecord,
): void {
const now = new Date().toISOString();
const ids = chatIds({
...request,
request_id: request.request_id || result.request_id,
session_id: request.session_id || result.session_id,
project_id: request.project_id || result.project_id,
});
const status = result.success ? "completed" : "failed";
const title = chatTitle(request);
const db = getDigitalEmployeeDb();
if (!db) return;
const tx = db.transaction(() => {
upsertPlan({
id: ids.planId,
title: `远程任务:${title}`,
objective: request.prompt || request.project_id || "管理端下发任务",
status,
payload: { request, result },
now,
});
upsertTask({
id: ids.taskId,
planId: ids.planId,
title,
status,
assignedSkillId: "qimingclaw-computer-control",
payload: { request, result },
now,
});
upsertRun({
id: ids.runId,
planId: ids.planId,
taskId: ids.taskId,
status,
payload: { request, result },
now,
});
finishRun(ids.runId, status, now);
persistPayloadArtifactsAndApprovals(ids, { request, result }, now);
insertEvent(
{
event_id: `${ids.runId}:${status}`,
kind: result.success ? "computer_chat_completed" : "computer_chat_failed",
occurred_at: now,
message: result.success
? `管理端任务完成session=${result.session_id || request.session_id || "unknown"}`
: `管理端任务失败:${result.message || result.code || "unknown error"}`,
plan_id: ids.planId,
task_id: ids.taskId,
},
ids.runId,
{ request, result },
);
});
tx();
}
export function recordDigitalEmployeeRuntimeEvent(
event: DigitalEmployeeRuntimeEventRecord,
): void {
const now = new Date().toISOString();
const ids = chatIds({
request_id: event.request_id,
session_id: event.session_id,
project_id: event.project_id,
prompt: event.message,
});
const status = event.status ?? "running";
const title = event.message || event.kind;
const db = getDigitalEmployeeDb();
if (!db) return;
const tx = db.transaction(() => {
upsertPlan({
id: ids.planId,
title: `运行事件:${title}`,
objective: title,
status,
payload: event.payload ?? event,
now,
});
upsertTask({
id: ids.taskId,
planId: ids.planId,
title,
status,
assignedSkillId: "qimingclaw-acp-session",
payload: event.payload ?? event,
now,
});
upsertRun({
id: ids.runId,
planId: ids.planId,
taskId: ids.taskId,
status,
payload: event.payload ?? event,
now,
});
if (status === "completed" || status === "failed") {
finishRun(ids.runId, status, now);
}
persistPayloadArtifactsAndApprovals(ids, event.payload ?? event, now);
insertEvent(
{
event_id: `${ids.runId}:${event.kind}:${Date.now()}`,
kind: event.kind,
occurred_at: now,
message: title,
plan_id: ids.planId,
task_id: ids.taskId,
},
ids.runId,
event.payload ?? event,
);
});
tx();
}
export function recordDigitalEmployeeGovernanceCommand(
command: DigitalEmployeeGovernanceCommandRecord,
): { planId: string; taskId: string; runId: string } {
const now = new Date().toISOString();
const commandId = command.commandId || `governance-${Date.now()}`;
const result = objectRecord(command.result) ?? {};
const payload = objectRecord(command.payload) ?? {};
const planId = stringValue(command.planId)
|| stringValue(result.plan_id)
|| stringValue(payload.plan_id)
|| `governance-plan-${safeId(commandId)}`;
const ids = {
planId,
taskId: `governance-task-${safeId(commandId)}`,
runId: `governance-run-${safeId(commandId)}`,
};
const status = governanceCommandStatus(command);
const payloadEnvelope = {
source: "qimingclaw-governance-command",
commandKind: command.commandKind,
commandId,
correlationId: command.correlationId,
planId,
scheduleId: command.scheduleId || stringValue(result.schedule_id) || null,
accepted: command.accepted !== false,
message: command.message,
error: command.error,
payload,
result,
};
const db = getDigitalEmployeeDb();
if (!db) return ids;
const existingPlan = readExistingPlanSummary(planId);
const title = stringValue(payload.title ?? result.title)
|| existingPlan?.title
|| governanceCommandTitle(command.commandKind);
const objective = stringValue(payload.objective ?? payload.description ?? payload.requested_action)
|| existingPlan?.objective
|| command.message
|| title;
const tx = db.transaction(() => {
upsertPlan({
id: ids.planId,
title,
objective,
status,
payload: payloadEnvelope,
now,
});
upsertTask({
id: ids.taskId,
planId: ids.planId,
title: governanceCommandTitle(command.commandKind),
status,
assignedSkillId: "qimingclaw-governance-command",
payload: payloadEnvelope,
now,
});
if (command.commandKind === "create_plan") {
upsertCreatedPlanTasks({
planId: ids.planId,
payload,
status: "pending",
commandId,
now,
});
}
upsertRun({
id: ids.runId,
planId: ids.planId,
taskId: ids.taskId,
status,
payload: payloadEnvelope,
now,
});
if (status !== "running" && status !== "pending") {
finishRun(ids.runId, status, now);
}
persistPayloadArtifactsAndApprovals(ids, payloadEnvelope, now);
insertEvent(
{
event_id: `${ids.runId}:${command.commandKind}:${safeId(commandId)}`,
kind: `governance_${command.commandKind}`,
occurred_at: now,
message: command.message || `${governanceCommandTitle(command.commandKind)} 已记录`,
plan_id: ids.planId,
task_id: ids.taskId,
},
ids.runId,
payloadEnvelope,
);
});
tx();
return ids;
}
function governanceCommandTitle(commandKind: string): string {
switch (commandKind) {
case "create_plan":
return "创建数字员工计划";
case "submit_plan":
return "提交数字员工计划";
case "approve_plan":
return "批准数字员工计划";
case "activate_plan":
return "激活数字员工计划";
case "create_schedule":
return "创建数字员工定时任务";
case "run_schedule_now":
return "立即运行数字员工计划";
case "pause_plan":
return "暂停数字员工计划";
case "cancel_plan":
return "取消数字员工计划";
default:
return `数字员工治理命令:${commandKind || "unknown"}`;
}
}
function readExistingPlanSummary(planId: string): { title: string; objective: string } | null {
const db = getDigitalEmployeeDb();
if (!db) return null;
try {
const row = db.prepare("SELECT title, objective FROM digital_plans WHERE id = ?").get(planId) as Record<string, unknown> | undefined;
if (!row) return null;
return {
title: stringValue(row.title),
objective: stringValue(row.objective),
};
} catch {
return null;
}
}
function upsertCreatedPlanTasks(input: {
planId: string;
payload: Record<string, unknown>;
status: string;
commandId: string;
now: string;
}): void {
extractPlanTaskInputs(input.payload).forEach((task, index) => {
const title = task.title || `计划任务 ${index + 1}`;
const taskId = task.id || `${input.planId}:task:${safeId(`${index + 1}-${title}`)}`;
upsertTask({
id: taskId,
planId: input.planId,
title,
status: input.status,
assignedSkillId: task.assignedSkillId,
payload: {
source: "qimingclaw-create-plan",
commandId: input.commandId,
stepId: task.stepId,
description: task.description,
dependsOn: task.dependsOn,
skills: task.skills,
original: task.original,
},
now: input.now,
});
});
}
function extractPlanTaskInputs(payload: Record<string, unknown>): Array<{
id: string;
stepId: string;
title: string;
description: string;
assignedSkillId: string;
dependsOn: string[];
skills: string[];
original: unknown;
}> {
const explicitTasks = unknownArray(payload.tasks)
.map((task, index) => planTaskInputFromRecord(objectRecord(task), index, ""));
const stepTasks = unknownArray(payload.steps).flatMap((step, stepIndex) => {
const stepRecord = objectRecord(step);
if (!stepRecord) return [];
const nestedTasks = unknownArray(stepRecord.tasks);
if (nestedTasks.length > 0) {
return nestedTasks.map((task, taskIndex) => planTaskInputFromRecord(
objectRecord(task),
taskIndex,
stringValue(stepRecord.step_id ?? stepRecord.id) || `step-${stepIndex + 1}`,
stepRecord,
));
}
return [planTaskInputFromRecord(stepRecord, stepIndex, stringValue(stepRecord.step_id ?? stepRecord.id) || `step-${stepIndex + 1}`)];
});
return dedupeByKey(
[...explicitTasks, ...stepTasks].filter((task): task is NonNullable<typeof task> => Boolean(task)),
(task) => task.id || `${task.stepId}:${task.title}`,
);
}
function planTaskInputFromRecord(
record: Record<string, unknown> | null,
index: number,
fallbackStepId: string,
parentStep?: Record<string, unknown>,
): ReturnType<typeof extractPlanTaskInputs>[number] | null {
if (!record) return null;
const stepId = stringValue(record.step_id ?? record.stepId) || fallbackStepId;
const title = stringValue(record.title ?? record.name ?? record.task_title)
|| stringValue(parentStep?.title ?? parentStep?.name)
|| `计划任务 ${index + 1}`;
const description = stringValue(record.description ?? record.summary)
|| stringValue(parentStep?.description ?? parentStep?.summary)
|| title;
const skills = normalizeSkillIds([
...unknownArray(record.skills),
...unknownArray(record.preferred_skills),
...unknownArray(parentStep?.skills),
...unknownArray(parentStep?.preferred_skills),
stringValue(record.assigned_skill_id ?? record.assignedSkillId),
]);
return {
id: stringValue(record.task_id ?? record.id),
stepId,
title,
description,
assignedSkillId: skills[0] ?? "qimingclaw-plan-template",
dependsOn: normalizeSkillIds(unknownArray(record.depends_on ?? parentStep?.depends_on)),
skills,
original: record,
};
}
function normalizeSkillIds(values: unknown[]): string[] {
return [...new Set(values
.map((value) => stringValue(value))
.filter(Boolean))];
}
function governanceCommandStatus(
command: DigitalEmployeeGovernanceCommandRecord,
): string {
if (command.accepted === false || command.error) return "failed";
const resultStatus = stringValue(command.result?.status);
if (resultStatus) return resultStatus;
switch (command.commandKind) {
case "activate_plan":
case "run_schedule_now":
return "running";
case "create_plan":
return "draft";
case "submit_plan":
return "reviewing";
case "approve_plan":
return "approved";
case "pause_plan":
return "paused";
case "cancel_plan":
return "cancelled";
default:
return "completed";
}
}
function persistSnapshotTables(
snapshot: DigitalEmployeeSnapshot,
event: DigitalEmployeeStateEvent | null,
): void {
const db = getDigitalEmployeeDb();
if (!db) return;
const now = snapshot.generatedAt;
const planStatus = snapshot.services.agent?.running || snapshot.services.mcp?.running
? "running"
: "pending";
const taskStatus = snapshot.services.agent?.running ? "running" : "pending";
const runStatus = planStatus === "running" ? "running" : "pending";
const planPayload = {
services: snapshot.services,
sessions: snapshot.sessions,
};
const taskPayload = {
service: "agent",
agent: snapshot.services.agent,
mcp: snapshot.services.mcp,
};
const runPayload = {
snapshot,
};
const tx = db.transaction(() => {
upsertPlan({
id: "qimingclaw-client-readiness",
title: "客户端运行状态巡检",
objective: "确认 qimingclaw 客户端、Agent、MCP 和远程接入能力是否处于可执行状态。",
status: planStatus,
payload: planPayload,
now,
});
upsertTask({
id: "task-agent-status",
planId: "qimingclaw-client-readiness",
title: "读取 Agent 服务状态",
status: taskStatus,
assignedSkillId: "qimingclaw-acp-session",
payload: taskPayload,
now,
});
upsertRun({
id: "run-service-snapshot",
planId: "qimingclaw-client-readiness",
taskId: "task-agent-status",
status: runStatus,
payload: runPayload,
now,
});
if (event) {
insertEvent(event, "run-service-snapshot", { snapshot });
}
});
tx();
}
function stringify(value: unknown): string {
return JSON.stringify(value ?? {});
}
function stringField(row: Record<string, unknown>, key: string): string {
const value = row[key];
return typeof value === "string" ? value : "";
}
function optionalStringField(
row: Record<string, unknown>,
key: string,
): string | null {
const value = row[key];
return typeof value === "string" && value.trim() ? value : null;
}
function parseStoredPayload(value: unknown): unknown {
if (typeof value !== "string" || !value.trim()) return {};
try {
return JSON.parse(value);
} catch {
return value;
}
}
function toPlanRecord(row: Record<string, unknown>): DigitalEmployeePlanRecord {
return {
id: stringField(row, "id"),
remoteId: optionalStringField(row, "remote_id"),
title: stringField(row, "title"),
objective: optionalStringField(row, "objective"),
status: stringField(row, "status"),
source: stringField(row, "source"),
payload: parseStoredPayload(row.payload),
syncStatus: stringField(row, "sync_status"),
lastSyncedAt: optionalStringField(row, "last_synced_at"),
syncError: optionalStringField(row, "sync_error"),
createdAt: stringField(row, "created_at"),
updatedAt: stringField(row, "updated_at"),
};
}
function toTaskRecord(row: Record<string, unknown>): DigitalEmployeeTaskRecord {
return {
id: stringField(row, "id"),
remoteId: optionalStringField(row, "remote_id"),
planId: optionalStringField(row, "plan_id"),
title: stringField(row, "title"),
status: stringField(row, "status"),
assignedSkillId: optionalStringField(row, "assigned_skill_id"),
payload: parseStoredPayload(row.payload),
syncStatus: stringField(row, "sync_status"),
lastSyncedAt: optionalStringField(row, "last_synced_at"),
syncError: optionalStringField(row, "sync_error"),
createdAt: stringField(row, "created_at"),
updatedAt: stringField(row, "updated_at"),
};
}
function toRunRecord(row: Record<string, unknown>): DigitalEmployeeRunRecord {
return {
id: stringField(row, "id"),
remoteId: optionalStringField(row, "remote_id"),
planId: optionalStringField(row, "plan_id"),
taskId: optionalStringField(row, "task_id"),
status: stringField(row, "status"),
startedAt: optionalStringField(row, "started_at"),
finishedAt: optionalStringField(row, "finished_at"),
payload: parseStoredPayload(row.payload),
syncStatus: stringField(row, "sync_status"),
lastSyncedAt: optionalStringField(row, "last_synced_at"),
syncError: optionalStringField(row, "sync_error"),
createdAt: stringField(row, "created_at"),
updatedAt: stringField(row, "updated_at"),
};
}
function toEventRecord(row: Record<string, unknown>): DigitalEmployeeEventRecord {
return {
id: stringField(row, "id"),
remoteId: optionalStringField(row, "remote_id"),
planId: optionalStringField(row, "plan_id"),
taskId: optionalStringField(row, "task_id"),
runId: optionalStringField(row, "run_id"),
kind: stringField(row, "kind"),
message: stringField(row, "message"),
payload: parseStoredPayload(row.payload),
syncStatus: stringField(row, "sync_status"),
lastSyncedAt: optionalStringField(row, "last_synced_at"),
syncError: optionalStringField(row, "sync_error"),
occurredAt: stringField(row, "occurred_at"),
createdAt: stringField(row, "created_at"),
};
}
function toArtifactRecord(
row: Record<string, unknown>,
): DigitalEmployeeArtifactRecord {
return {
id: stringField(row, "id"),
remoteId: optionalStringField(row, "remote_id"),
planId: optionalStringField(row, "plan_id"),
taskId: optionalStringField(row, "task_id"),
runId: optionalStringField(row, "run_id"),
kind: stringField(row, "kind"),
name: optionalStringField(row, "name"),
uri: optionalStringField(row, "uri"),
payload: parseStoredPayload(row.payload),
syncStatus: stringField(row, "sync_status"),
lastSyncedAt: optionalStringField(row, "last_synced_at"),
syncError: optionalStringField(row, "sync_error"),
createdAt: stringField(row, "created_at"),
};
}
function toApprovalRecord(
row: Record<string, unknown>,
): DigitalEmployeeApprovalRecord {
return {
id: stringField(row, "id"),
remoteId: optionalStringField(row, "remote_id"),
planId: optionalStringField(row, "plan_id"),
taskId: optionalStringField(row, "task_id"),
runId: optionalStringField(row, "run_id"),
title: stringField(row, "title"),
status: stringField(row, "status"),
payload: parseStoredPayload(row.payload),
syncStatus: stringField(row, "sync_status"),
lastSyncedAt: optionalStringField(row, "last_synced_at"),
syncError: optionalStringField(row, "sync_error"),
createdAt: stringField(row, "created_at"),
updatedAt: stringField(row, "updated_at"),
};
}
function countPendingSyncItems(): number {
const db = getDigitalEmployeeDb();
if (!db) return 0;
const row = db.prepare(`
SELECT COUNT(*) AS count FROM digital_sync_outbox WHERE status = 'pending'
`).get() as { count: number } | undefined;
return row?.count ?? 0;
}
function syncOutboxId(entityType: string, entityId: string, operation = "upsert"): string {
return `${entityType}:${operation}:${entityId}`;
}
function markOutbox(
entityType: string,
entityId: string,
operation: string,
payload: unknown,
now: string,
): void {
const db = getDigitalEmployeeDb();
if (!db) return;
db.prepare(`
INSERT INTO digital_sync_outbox (
id, entity_type, entity_id, operation, payload, status, attempts, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, 'pending', 0, ?, ?)
ON CONFLICT(id) DO UPDATE SET
payload = excluded.payload,
status = 'pending',
updated_at = excluded.updated_at,
error = NULL
`).run(
syncOutboxId(entityType, entityId, operation),
entityType,
entityId,
operation,
stringify(payload),
now,
now,
);
}
function upsertPlan(input: {
id: string;
title: string;
objective: string;
status: string;
payload: unknown;
now: string;
}): void {
const db = getDigitalEmployeeDb();
if (!db) return;
db.prepare(`
INSERT INTO digital_plans (
id, title, objective, status, source, payload, sync_status, created_at, updated_at
) VALUES (?, ?, ?, ?, 'qimingclaw', ?, 'pending', ?, ?)
ON CONFLICT(id) DO UPDATE SET
title = excluded.title,
objective = excluded.objective,
status = excluded.status,
payload = excluded.payload,
sync_status = CASE
WHEN digital_plans.sync_status = 'synced' THEN 'pending'
ELSE digital_plans.sync_status
END,
updated_at = excluded.updated_at
`).run(
input.id,
input.title,
input.objective,
input.status,
stringify(input.payload),
input.now,
input.now,
);
markOutbox("plan", input.id, "upsert", input, input.now);
}
function upsertTask(input: {
id: string;
planId: string;
title: string;
status: string;
assignedSkillId: string;
payload: unknown;
now: string;
}): void {
const db = getDigitalEmployeeDb();
if (!db) return;
db.prepare(`
INSERT INTO digital_tasks (
id, plan_id, title, status, assigned_skill_id, payload, sync_status, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?)
ON CONFLICT(id) DO UPDATE SET
plan_id = excluded.plan_id,
title = excluded.title,
status = excluded.status,
assigned_skill_id = excluded.assigned_skill_id,
payload = excluded.payload,
sync_status = CASE
WHEN digital_tasks.sync_status = 'synced' THEN 'pending'
ELSE digital_tasks.sync_status
END,
updated_at = excluded.updated_at
`).run(
input.id,
input.planId,
input.title,
input.status,
input.assignedSkillId,
stringify(input.payload),
input.now,
input.now,
);
markOutbox("task", input.id, "upsert", input, input.now);
}
function upsertRun(input: {
id: string;
planId: string;
taskId: string;
status: string;
payload: unknown;
now: string;
}): void {
const db = getDigitalEmployeeDb();
if (!db) return;
db.prepare(`
INSERT INTO digital_runs (
id, plan_id, task_id, status, started_at, payload, sync_status, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?)
ON CONFLICT(id) DO UPDATE SET
plan_id = excluded.plan_id,
task_id = excluded.task_id,
status = excluded.status,
payload = excluded.payload,
sync_status = CASE
WHEN digital_runs.sync_status = 'synced' THEN 'pending'
ELSE digital_runs.sync_status
END,
updated_at = excluded.updated_at
`).run(
input.id,
input.planId,
input.taskId,
input.status,
input.now,
stringify(input.payload),
input.now,
input.now,
);
markOutbox("run", input.id, "upsert", input, input.now);
}
function finishRun(runId: string, status: string, now: string): void {
const db = getDigitalEmployeeDb();
if (!db) return;
db.prepare(`
UPDATE digital_runs
SET status = ?, finished_at = ?, updated_at = ?, sync_status = 'pending'
WHERE id = ?
`).run(status, now, now, runId);
}
function upsertArtifact(input: {
id: string;
planId: string;
taskId: string;
runId: string;
kind: string;
name: string | null;
uri: string | null;
payload: unknown;
now: string;
}): void {
const db = getDigitalEmployeeDb();
if (!db) return;
db.prepare(`
INSERT INTO digital_artifacts (
id, plan_id, task_id, run_id, kind, name, uri, payload, sync_status, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?)
ON CONFLICT(id) DO UPDATE SET
plan_id = excluded.plan_id,
task_id = excluded.task_id,
run_id = excluded.run_id,
kind = excluded.kind,
name = excluded.name,
uri = excluded.uri,
payload = excluded.payload,
sync_status = CASE
WHEN digital_artifacts.sync_status = 'synced' THEN 'pending'
ELSE digital_artifacts.sync_status
END
`).run(
input.id,
input.planId,
input.taskId,
input.runId,
input.kind,
input.name,
input.uri,
stringify(input.payload),
input.now,
);
markOutbox("artifact", input.id, "upsert", input, input.now);
}
function upsertApproval(input: {
id: string;
planId: string;
taskId: string;
runId: string;
title: string;
status: string;
payload: unknown;
now: string;
}): void {
const db = getDigitalEmployeeDb();
if (!db) return;
db.prepare(`
INSERT INTO digital_approvals (
id, plan_id, task_id, run_id, title, status, payload, sync_status, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?)
ON CONFLICT(id) DO UPDATE SET
plan_id = excluded.plan_id,
task_id = excluded.task_id,
run_id = excluded.run_id,
title = excluded.title,
status = excluded.status,
payload = excluded.payload,
sync_status = CASE
WHEN digital_approvals.sync_status = 'synced' THEN 'pending'
ELSE digital_approvals.sync_status
END,
updated_at = excluded.updated_at
`).run(
input.id,
input.planId,
input.taskId,
input.runId,
input.title,
input.status,
stringify(input.payload),
input.now,
input.now,
);
markOutbox("approval", input.id, "upsert", input, input.now);
}
function insertEvent(
event: DigitalEmployeeStateEvent,
runId: string,
payload: unknown,
): void {
const db = getDigitalEmployeeDb();
if (!db) return;
db.prepare(`
INSERT OR IGNORE INTO digital_events (
id, plan_id, task_id, run_id, kind, message, payload, sync_status, occurred_at, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?)
`).run(
event.event_id,
event.plan_id,
event.task_id,
runId,
event.kind,
event.message,
stringify(payload),
event.occurred_at,
event.occurred_at,
);
markOutbox("event", event.event_id, "insert", event, event.occurred_at);
}
function persistPayloadArtifactsAndApprovals(
ids: { planId: string; taskId: string; runId: string },
payload: unknown,
now: string,
): void {
const sources = payloadSources(payload);
const artifacts = sources.flatMap((source) => extractArtifacts(source));
const approvals = sources.flatMap((source) => extractApprovals(source));
dedupeByKey(artifacts, (artifact) => artifact.key).slice(0, 20).forEach((artifact, index) => {
upsertArtifact({
id: `${ids.runId}:artifact:${safeId(artifact.key || String(index + 1))}`,
planId: ids.planId,
taskId: ids.taskId,
runId: ids.runId,
kind: artifact.kind,
name: artifact.name,
uri: artifact.uri,
payload: artifact.payload,
now,
});
});
dedupeByKey(approvals, (approval) => approval.key).slice(0, 20).forEach((approval, index) => {
upsertApproval({
id: `${ids.runId}:approval:${safeId(approval.key || String(index + 1))}`,
planId: ids.planId,
taskId: ids.taskId,
runId: ids.runId,
title: approval.title,
status: approval.status,
payload: approval.payload,
now,
});
});
}
function payloadSources(payload: unknown): unknown[] {
const record = objectRecord(payload);
if (!record) return [payload];
return [
payload,
record.result,
record.response,
record.output,
record.payload,
].filter((item) => item !== undefined && item !== null);
}
interface ExtractedArtifact {
key: string;
kind: string;
name: string | null;
uri: string | null;
payload: unknown;
}
function extractArtifacts(payload: unknown): ExtractedArtifact[] {
const record = objectRecord(payload);
if (!record) return typeof payload === "string" && looksLikeArtifactUri(payload)
? [artifactFromValue(payload, 0)]
: [];
const values = [
...unknownArray(record.artifacts),
...unknownArray(record.outputs),
...unknownArray(record.files),
...unknownArray(record.attachments),
];
for (const key of ["artifact", "outputPath", "output_path", "filePath", "file_path", "path", "uri", "url"]) {
if (record[key]) values.push(record[key]);
}
return values
.map((value, index) => artifactFromValue(value, index))
.filter((artifact) => artifact.name || artifact.uri);
}
function artifactFromValue(value: unknown, index: number): ExtractedArtifact {
const record = objectRecord(value);
if (record) {
const uri = stringValue(record.uri ?? record.url ?? record.path ?? record.filePath ?? record.file_path ?? record.outputPath ?? record.output_path);
const name = stringValue(record.name ?? record.title ?? record.filename) || artifactName(uri) || `产物 ${index + 1}`;
const kind = stringValue(record.kind ?? record.type) || "artifact";
return {
key: stringValue(record.id ?? record.artifact_id) || uri || name,
kind,
name,
uri: uri || null,
payload: value,
};
}
const text = String(value ?? "").trim();
return {
key: text,
kind: "artifact",
name: artifactName(text) || `产物 ${index + 1}`,
uri: looksLikeArtifactUri(text) ? text : null,
payload: value,
};
}
interface ExtractedApproval {
key: string;
title: string;
status: string;
payload: unknown;
}
function extractApprovals(payload: unknown): ExtractedApproval[] {
const record = objectRecord(payload);
if (!record) return [];
const values = [
...unknownArray(record.approvals),
...unknownArray(record.approval_requests),
...unknownArray(record.approvalRequests),
...unknownArray(record.decisions),
];
if (record.needApproval === true || record.need_approval === true || record.requiresApproval === true) {
values.push(record);
}
return values.map((value, index) => approvalFromValue(value, index));
}
function approvalFromValue(value: unknown, index: number): ExtractedApproval {
const record = objectRecord(value);
if (!record) {
return {
key: String(index + 1),
title: `待确认事项 ${index + 1}`,
status: "pending",
payload: value,
};
}
const title = stringValue(record.title ?? record.message ?? record.reason ?? record.summary)
|| `待确认事项 ${index + 1}`;
return {
key: stringValue(record.id ?? record.approval_id ?? record.key) || title,
title,
status: stringValue(record.status) || "pending",
payload: value,
};
}
function objectRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" && !Array.isArray(value)
? value as Record<string, unknown>
: null;
}
function unknownArray(value: unknown): unknown[] {
return Array.isArray(value) ? value : [];
}
function stringValue(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
}
function looksLikeArtifactUri(value: string): boolean {
return /^(file|https?):\/\//i.test(value) || value.includes("/") || value.includes("\\");
}
function artifactName(value: string): string {
if (!value) return "";
const normalized = value.split(/[?#]/)[0] || value;
return normalized.split(/[\\/]/).filter(Boolean).pop() ?? "";
}
function dedupeByKey<T>(items: T[], keyOf: (item: T) => string): T[] {
const seen = new Set<string>();
return items.filter((item) => {
const key = keyOf(item);
if (!key || seen.has(key)) return false;
seen.add(key);
return true;
});
}
function chatIds(request: DigitalEmployeeChatRequestRecord): {
planId: string;
taskId: string;
runId: string;
} {
const base = safeId(request.request_id || request.session_id || request.project_id || `${Date.now()}`);
return {
planId: `computer-chat-plan-${base}`,
taskId: `computer-chat-task-${base}`,
runId: `computer-chat-run-${base}`,
};
}
function chatTitle(request: DigitalEmployeeChatRequestRecord): string {
const prompt = (request.prompt || "").replace(/\s+/g, " ").trim();
if (prompt) return prompt.length > 48 ? `${prompt.slice(0, 48)}...` : prompt;
return request.project_id ? `项目 ${request.project_id}` : "管理端下发任务";
}
function safeId(value: string): string {
return value.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
}
function eventFromSnapshot(
snapshot: DigitalEmployeeSnapshot,
previous: DigitalEmployeeSnapshot | null,
): DigitalEmployeeStateEvent | null {
const currentSummary = summarizeSnapshot(snapshot);
const previousSummary = previous ? summarizeSnapshot(previous) : "";
if (currentSummary === previousSummary) {
return null;
}
return {
event_id: `qimingclaw-snapshot-${Date.now()}`,
kind: "service_snapshot",
occurred_at: snapshot.generatedAt,
message: currentSummary,
plan_id: "qimingclaw-client-readiness",
task_id: "task-agent-status",
};
}
function summarizeSnapshot(snapshot: DigitalEmployeeSnapshot): string {
const serviceNames = [
["agent", "Agent"],
["mcp", "MCP"],
["computerServer", "Computer Server"],
["lanproxy", "Lanproxy"],
["fileServer", "File Server"],
] as const;
const services = serviceNames.map(([key, label]) => {
const status = snapshot.services[key];
return `${label}:${status?.running ? "running" : "stopped"}`;
});
services.push(`sessions:${snapshot.sessions.active}/${snapshot.sessions.total}`);
return services.join(";");
}