1390 lines
39 KiB
TypeScript
1390 lines
39 KiB
TypeScript
import { 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 DigitalEmployeeSnapshot {
|
||
generatedAt: string;
|
||
services: Record<string, DigitalEmployeeServiceStatus | null>;
|
||
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>>;
|
||
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;
|
||
}
|
||
|
||
function defaultState(): DigitalEmployeeState {
|
||
return {
|
||
version: 1,
|
||
updatedAt: null,
|
||
lastSnapshot: null,
|
||
events: [],
|
||
};
|
||
}
|
||
|
||
function defaultUiState(): DigitalEmployeeUiState {
|
||
return {
|
||
selectedDutyIdsByDate: {},
|
||
confirmedDates: {},
|
||
reportScheduleTime: "18:00",
|
||
reportGeneratedAtByDate: {},
|
||
decisionResultsByDate: {},
|
||
profile: {
|
||
name: "飞天数字员工",
|
||
company: "qimingclaw 客户端",
|
||
position: "客户端任务执行员",
|
||
currentStatus: "working",
|
||
},
|
||
};
|
||
}
|
||
|
||
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 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),
|
||
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}`;
|
||
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 = getDb();
|
||
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 payload = storedPayloadRecord
|
||
? {
|
||
...storedPayloadRecord,
|
||
decision,
|
||
decidedAt: now,
|
||
previousStatus,
|
||
}
|
||
: {
|
||
value: storedPayload,
|
||
decision,
|
||
decidedAt: now,
|
||
previousStatus,
|
||
};
|
||
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 = getDb();
|
||
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 = getDb();
|
||
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 = getDb();
|
||
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 = getDb();
|
||
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();
|
||
}
|
||
|
||
function persistSnapshotTables(
|
||
snapshot: DigitalEmployeeSnapshot,
|
||
event: DigitalEmployeeStateEvent | null,
|
||
): void {
|
||
const db = getDb();
|
||
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 = getDb();
|
||
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 = getDb();
|
||
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 = getDb();
|
||
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 = getDb();
|
||
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 = getDb();
|
||
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 = getDb();
|
||
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 = getDb();
|
||
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 = getDb();
|
||
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 = getDb();
|
||
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(";");
|
||
}
|