Files
qiming/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/qimingclawAdapter.ts
2026-06-09 11:06:25 +08:00

2902 lines
108 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 type {
ApprovalEntry,
ArtifactEntry,
CanonicalEvent,
CronJob,
CronRun,
DashboardResponse,
DebugPlan,
DebugRun,
DebugStoreResponse,
DebugTask,
DigitalEmployeeDecision,
DigitalEmployeeDuty,
DigitalEmployeeManagedService,
DigitalEmployeePlanAgentState,
DigitalEmployeeRunDetail,
DigitalEmployeeTaskAgentState,
DigitalEmployeeTaskExecutionSummary,
DigitalEmployeeWorkEvent,
DigitalEmployeeWorkdayActionRequest,
DigitalEmployeeWorkdayProjection,
FlowResponse,
GovernanceCommandRequest,
GovernanceCommandResponse,
PlanDispatchResponse,
PlanStepSummary,
SkillSpec,
} from '@/types/api';
declare global {
interface Window {
__QIMINGCLAW_EMBEDDED_DIGITAL__?: boolean;
QimingClawBridge?: {
digital?: {
getSnapshot?: () => Promise<QimingclawSnapshot>;
getRuntimeRecords?: () => Promise<QimingclawRuntimeRecords>;
getUiState?: () => Promise<AdapterState>;
getSkillCapabilities?: () => Promise<QimingclawSkillCapabilities>;
getPlanTemplates?: () => Promise<QimingclawPlanTemplateList>;
saveUiState?: (update: { state: AdapterState; action?: string; date?: string; metadata?: Record<string, unknown> }) => Promise<AdapterState>;
getSyncStatus?: () => Promise<QimingclawSyncStatus>;
flushSync?: () => Promise<QimingclawSyncStatus>;
getCronSettings?: () => Promise<QimingclawCronSettings>;
saveCronSettings?: (patch: Partial<QimingclawCronSettings>) => Promise<QimingclawCronSettings>;
runSchedulerCheck?: () => Promise<{ activated: number; due?: number; skipped?: number }>;
runScheduleNow?: (scheduleId: string) => Promise<{ ok?: boolean; scheduleRunId?: string; error?: string }>;
getScheduleRuns?: (scheduleId: string, limit?: number) => Promise<QimingclawScheduleRunRecord[]>;
recordGovernanceCommand?: (command: QimingclawGovernanceCommandRecord) => Promise<unknown>;
respondPermission?: (sessionId: string, permissionId: string, response: 'once' | 'always' | 'reject') => Promise<{ success?: boolean; error?: string }>;
};
};
}
}
const STATE_KEY = 'qimingclaw:digital-employee-adapter-state';
const DEFAULT_REPORT_TIME = '18:00';
interface AdapterState {
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;
};
}
interface QimingclawServiceStatus {
running?: boolean;
error?: string;
engineType?: string;
port?: number;
serverCount?: number;
serverNames?: string[];
}
interface QimingclawSyncStatus {
running: boolean;
syncing: boolean;
enabled: boolean;
endpoint: string | null;
missingCredentials?: string[];
pending: number;
failed: number;
lastSyncAt: string | null;
lastSyncError: string | null;
failureSummary?: QimingclawSyncFailureSummary;
recentFailures?: QimingclawSyncFailure[];
}
interface QimingclawSyncFailureSummary {
total: number;
dueForRetry: number;
backoff: number;
byEntityType: Array<{ entityType: string; count: number }>;
}
interface QimingclawSyncAttempt {
attemptNo: number;
status: string;
error: string | null;
endpoint?: string | null;
startedAt: string;
finishedAt: string;
}
interface QimingclawSyncFailure {
id: string;
entityType: string;
entityId: string;
operation: string;
attempts: number;
lastAttemptAt: string | null;
nextRetryAt?: string | null;
dueForRetry?: boolean;
managementRecordUrl?: string | null;
attemptHistory?: QimingclawSyncAttempt[];
error: string | null;
createdAt: string;
updatedAt: string;
}
interface QimingclawSnapshot {
generatedAt: string;
services: Record<string, QimingclawServiceStatus | null>;
managedServices?: QimingclawManagedService[] | null;
sessions: {
total: number;
active: number;
items: Array<{ id: string; title?: string; status?: string; engineType?: string; projectId?: string }>;
};
sync?: QimingclawSyncStatus | null;
runtime?: QimingclawRuntimeRecords | null;
uiState?: AdapterState | null;
planTemplates?: QimingclawPlanTemplateRecord[] | null;
}
interface QimingclawManagedService {
serviceId: string;
name: string;
kind: string;
status: string;
statusLabel: string;
running: boolean;
error?: string | null;
requiresHumanAction: boolean;
dependentTasks: string[];
restartCount: number;
lastSeenAt: string;
instance?: Record<string, unknown>;
lease?: Record<string, unknown>;
}
interface QimingclawPlanTemplateList {
generatedAt: string;
templateDir: string;
templates: QimingclawPlanTemplateRecord[];
}
interface QimingclawPlanTemplateRecord extends DebugPlan {
source: 'qimingclaw-plan-template';
template_path: string;
updated_at: string;
}
interface QimingclawGovernanceCommandRecord {
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;
}
interface QimingclawRuntimeRecords {
generatedAt: string;
plans: QimingclawPlanRecord[];
planSteps?: QimingclawPlanStepRecord[];
schedules?: QimingclawScheduleRecord[];
scheduleRuns?: QimingclawScheduleRunRecord[];
tasks: QimingclawTaskRecord[];
runs: QimingclawRunRecord[];
events: QimingclawEventRecord[];
artifacts?: QimingclawArtifactRecord[];
approvals?: QimingclawApprovalRecord[];
}
interface QimingclawCronSettings {
enabled: boolean;
catch_up_on_startup: boolean;
max_run_history: number;
max_catch_up_runs: number;
max_concurrent_schedule_runs: number;
}
interface QimingclawScheduleRecord {
id: string;
remoteId?: string | null;
planId?: string | null;
name: string;
description?: string | null;
cronExpr: string;
timezone: string;
status: string;
enabled: boolean;
prompt?: string | null;
command?: string | null;
payload: unknown;
nextRunAt?: string | null;
lastRunAt?: string | null;
catchUpOnStartup: boolean;
maxCatchUpRuns: number;
maxRunHistory: number;
failureCount: number;
lastError?: string | null;
syncStatus: string;
createdAt: string;
updatedAt: string;
deletedAt?: string | null;
}
interface QimingclawScheduleRunRecord {
id: string;
scheduleId: string;
planId?: string | null;
runId?: string | null;
dueAt: string;
startedAt?: string | null;
finishedAt?: string | null;
status: string;
attemptNo: number;
triggerKind: string;
error?: string | null;
payload: unknown;
createdAt: string;
updatedAt: string;
}
interface QimingclawPlanRecord {
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;
}
interface QimingclawPlanStepRecord {
id: string;
remoteId?: string | null;
planId: string;
title: string;
status: string;
seq: number;
dependsOn: string[];
preferredSkillIds: string[];
payload: unknown;
syncStatus: string;
lastSyncedAt?: string | null;
syncError?: string | null;
createdAt: string;
updatedAt: string;
}
interface QimingclawTaskRecord {
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;
}
interface QimingclawRunRecord {
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;
}
interface QimingclawEventRecord {
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;
}
interface QimingclawArtifactRecord {
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;
}
interface QimingclawApprovalRecord {
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;
}
interface QimingclawSkillCapabilities {
generatedAt: string;
skills: SkillSpec[];
}
const SKILLS: SkillSpec[] = [
{
skill_id: 'qimingclaw-acp-session',
name: 'ACP 会话执行',
version: '1.0.0',
enabled: true,
description: '通过 qimingclaw 的 ACP 引擎创建会话、执行任务并跟踪结果。',
category: '执行',
capability_level: 'core',
},
{
skill_id: 'qimingclaw-mcp-tools',
name: 'MCP 工具编排',
version: '1.0.0',
enabled: true,
description: '读取已配置 MCP 服务与工具,作为数字员工的工具能力池。',
category: '工具',
capability_level: 'core',
},
{
skill_id: 'qimingclaw-computer-control',
name: '客户端电脑控制',
version: '1.0.0',
enabled: true,
description: '承接 /computer/chat 执行入口,面向远程任务和本地自动化操作。',
category: '桌面',
capability_level: 'specialized',
},
{
skill_id: 'qimingclaw-artifact-report',
name: '产物与日报整理',
version: '1.0.0',
enabled: true,
description: '把执行记录、关键产物和异常汇总成数字员工日报。',
category: '汇报',
capability_level: 'standard',
},
];
const PLANS: DebugPlan[] = [
{
plan_id: 'qimingclaw-client-readiness',
title: '客户端运行状态巡检',
objective: '确认 qimingclaw 客户端、Agent、MCP 和远程接入能力是否处于可执行状态。',
status: 'running',
created_at: todayAt(9, 0),
steps: [
{
step_id: 'agent-status',
title: '检查 Agent 引擎状态',
depends_on: [],
preferred_skills: ['qimingclaw-acp-session'],
tasks: [{ task_id: 'task-agent-status', title: '读取 Agent 服务状态', status: 'running' }],
},
{
step_id: 'mcp-status',
title: '检查 MCP 工具能力',
depends_on: ['agent-status'],
preferred_skills: ['qimingclaw-mcp-tools'],
tasks: [{ task_id: 'task-mcp-status', title: '同步 MCP 工具列表', status: 'pending' }],
},
],
},
{
plan_id: 'qimingclaw-remote-task-ingress',
title: '远程任务接入准备',
objective: '检查 lanproxy、computer server 和文件服务,为后端下发数字员工任务做准备。',
status: 'pending',
created_at: todayAt(9, 30),
steps: [
{
step_id: 'computer-server',
title: '确认 /computer/chat 执行入口',
depends_on: [],
preferred_skills: ['qimingclaw-computer-control'],
tasks: [{ task_id: 'task-computer-server', title: '检查电脑控制服务', status: 'pending' }],
},
{
step_id: 'file-artifact',
title: '准备产物回传目录',
depends_on: ['computer-server'],
preferred_skills: ['qimingclaw-artifact-report'],
tasks: [{ task_id: 'task-file-artifact', title: '整理执行产物通道', status: 'pending' }],
},
],
},
{
plan_id: 'qimingclaw-daily-report',
title: '数字员工日报生成',
objective: '汇总今日执行记录、服务状态和待介入事项,生成面向业务的日报。',
status: 'pending',
created_at: todayAt(17, 30),
steps: [
{
step_id: 'collect-events',
title: '汇总运行事件',
depends_on: [],
preferred_skills: ['qimingclaw-artifact-report'],
tasks: [{ task_id: 'task-collect-events', title: '整理今日运行明细', status: 'pending' }],
},
{
step_id: 'write-report',
title: '生成日报摘要',
depends_on: ['collect-events'],
preferred_skills: ['qimingclaw-artifact-report'],
tasks: [{ task_id: 'task-write-report', title: '生成数字员工日报', status: 'pending' }],
},
],
},
];
const PLAN_SCHEDULES: Record<string, { expression: string; nextRun: string; scheduleText: string }> = {
'qimingclaw-client-readiness': {
expression: '0 9 * * *',
nextRun: todayAt(9, 0),
scheduleText: '已定时 09:00 执行',
},
'qimingclaw-remote-task-ingress': {
expression: '30 9 * * *',
nextRun: todayAt(9, 30),
scheduleText: '已定时 09:30 执行',
},
'qimingclaw-daily-report': {
expression: '0 18 * * *',
nextRun: todayAt(18, 0),
scheduleText: '已定时 18:00 执行',
},
};
export function isQimingclawDigitalApiEnabled(): boolean {
return window.__QIMINGCLAW_EMBEDDED_DIGITAL__ === true;
}
export async function handleQimingclawDigitalApi<T>(
path: string,
options: RequestInit = {},
): Promise<T | undefined> {
if (!isQimingclawDigitalApiEnabled()) return undefined;
const url = new URL(path, window.location.origin);
const method = (options.method || 'GET').toUpperCase();
const pathname = url.pathname;
if (method === 'GET' && pathname === '/api/digital-employee/workday') {
return buildWorkday(url.searchParams.get('date') || todayInputDate(), await readQimingclawSnapshot()) as T;
}
if (method === 'POST' && pathname === '/api/digital-employee/workday/actions') {
return await handleWorkdayAction(parseJsonBody<DigitalEmployeeWorkdayActionRequest>(options.body), await readQimingclawSnapshot()) as T;
}
if (method === 'POST' && pathname === '/api/governance/command') {
return await handleGovernanceCommand(parseJsonBody<GovernanceCommandRequest>(options.body), await readQimingclawSnapshot()) as T;
}
if (method === 'GET' && pathname === '/api/scheduler/jobs') {
return { jobs: buildSchedulerJobs(await readQimingclawSnapshot()) } as T;
}
if (method === 'GET' && pathname === '/api/cron') {
return { jobs: buildSchedulerJobs(await readQimingclawSnapshot()) } as T;
}
if (method === 'POST' && pathname === '/api/cron') {
return await handleCronCreate(parseJsonBody<Record<string, unknown>>(options.body), await readQimingclawSnapshot()) as T;
}
if (method === 'GET' && pathname === '/api/cron/settings') {
return await readCronSettings() as T;
}
if (method === 'PATCH' && pathname === '/api/cron/settings') {
const patch = parseJsonBody<Partial<QimingclawCronSettings>>(options.body);
return await saveCronSettings(patch) as T;
}
if (method === 'POST' && pathname === '/api/cron/check') {
const result = await window.QimingClawBridge?.digital?.runSchedulerCheck?.();
return { ok: true, activated: result?.activated ?? 0, ...result } as T;
}
const cronRunMatch = pathname.match(/^\/api\/cron\/([^/]+)\/runs$/);
if (method === 'GET' && cronRunMatch) {
const runs = await readCronRuns(decodeURIComponent(cronRunMatch[1] || ''), Number(url.searchParams.get('limit')) || 20);
return { runs } as T;
}
const cronJobMatch = pathname.match(/^\/api\/cron\/([^/]+)$/);
if ((method === 'PATCH' || method === 'DELETE') && cronJobMatch) {
const scheduleId = decodeURIComponent(cronJobMatch[1] || '');
if (method === 'DELETE') {
await handleCronCommand('delete_schedule', scheduleId, {}, await readQimingclawSnapshot());
return undefined as T;
}
return await handleCronPatch(scheduleId, parseJsonBody<Record<string, unknown>>(options.body), await readQimingclawSnapshot()) as T;
}
if (method === 'GET' && pathname === '/api/debug/plans') {
return { plans: filterPlans(url.searchParams.get('q'), await readQimingclawSnapshot()) } as T;
}
if (method === 'GET' && pathname === '/api/debug/tasks') {
const planId = url.searchParams.get('plan_id');
return buildTasksResponse(planId, await readQimingclawSnapshot()) as T;
}
if (method === 'GET' && pathname === '/api/debug/store') {
return buildDebugStore(await readQimingclawSnapshot()) as T;
}
if (method === 'GET' && pathname === '/api/dashboard') {
return buildDashboard(await readQimingclawSnapshot()) as T;
}
if (method === 'GET' && pathname === '/api/artifact') {
return { artifacts: buildArtifacts(await readQimingclawSnapshot()) } as T;
}
if (method === 'GET' && pathname.startsWith('/api/debug/plan/') && pathname.endsWith('/flow')) {
return buildPlanFlow(decodeURIComponent(pathname.split('/')[4] || ''), await readQimingclawSnapshot()) as T;
}
if (method === 'GET' && pathname.startsWith('/api/debug/task/') && pathname.endsWith('/flow')) {
return buildTaskFlow(decodeURIComponent(pathname.split('/')[4] || ''), await readQimingclawSnapshot()) as T;
}
if (method === 'POST' && pathname.startsWith('/api/debug/plan/') && pathname.endsWith('/dispatch')) {
return await dispatchPlan(decodeURIComponent(pathname.split('/')[4] || ''), await readQimingclawSnapshot()) as T;
}
if (method === 'GET' && pathname.startsWith('/api/plan/') && pathname.endsWith('/messages')) {
return { messages: buildPlanMessages(decodeURIComponent(pathname.split('/')[3] || ''), await readQimingclawSnapshot()) } as T;
}
if (method === 'GET' && pathname === '/api/skill') {
return { skills: await buildSkills() } as T;
}
if (method === 'POST' && pathname.startsWith('/api/skill/') && pathname.endsWith('/enabled')) {
const skillId = decodeURIComponent(pathname.split('/')[3] || '');
const body = parseJsonBody<{ enabled?: boolean }>(options.body);
return { ok: true, skill_id: skillId, enabled: body.enabled ?? true } as T;
}
return undefined;
}
function defaultState(): AdapterState {
return {
selectedDutyIdsByDate: {},
confirmedDates: {},
reportScheduleTime: DEFAULT_REPORT_TIME,
reportGeneratedAtByDate: {},
decisionResultsByDate: {},
consoleRole: 'sales',
rolePreferences: {},
profile: {
name: '飞天数字员工',
company: 'qimingclaw 客户端',
position: '客户端任务执行员',
currentStatus: 'working',
},
};
}
function readState(): AdapterState {
try {
const raw = window.localStorage.getItem(STATE_KEY);
return raw ? { ...defaultState(), ...JSON.parse(raw) as Partial<AdapterState> } : defaultState();
} catch {
return defaultState();
}
}
function writeState(state: AdapterState): void {
window.localStorage.setItem(STATE_KEY, JSON.stringify(state));
}
async function readQimingclawSnapshot(): Promise<QimingclawSnapshot | null> {
try {
const snapshot = await window.QimingClawBridge?.digital?.getSnapshot?.() ?? null;
if (!snapshot) return null;
const [sync, runtime, uiState, planTemplates] = await Promise.all([
window.QimingClawBridge?.digital?.getSyncStatus?.().catch(() => null) ?? null,
window.QimingClawBridge?.digital?.getRuntimeRecords?.().catch(() => null) ?? null,
readBridgeUiState(),
window.QimingClawBridge?.digital?.getPlanTemplates?.().catch(() => null) ?? null,
]);
return { ...snapshot, sync, runtime, uiState, planTemplates: planTemplates?.templates ?? [] };
} catch {
return null;
}
}
async function readBridgeUiState(): Promise<AdapterState | null> {
try {
return await window.QimingClawBridge?.digital?.getUiState?.() ?? null;
} catch {
return null;
}
}
async function readBridgeSkillCapabilities(): Promise<SkillSpec[]> {
try {
const result = await window.QimingClawBridge?.digital?.getSkillCapabilities?.() ?? null;
return Array.isArray(result?.skills) ? result.skills : [];
} catch {
return [];
}
}
async function buildSkills(): Promise<SkillSpec[]> {
return dedupeSkills([...SKILLS, ...await readBridgeSkillCapabilities()]);
}
function dedupeSkills(skills: SkillSpec[]): SkillSpec[] {
const seen = new Set<string>();
const result: SkillSpec[] = [];
for (const skill of skills) {
const skillId = typeof skill.skill_id === 'string' ? skill.skill_id.trim() : '';
if (!skillId || seen.has(skillId)) continue;
seen.add(skillId);
result.push({ ...skill, skill_id: skillId });
}
return result;
}
async function saveState(
state: AdapterState,
action?: string,
date?: string,
metadata?: Record<string, unknown>,
): Promise<AdapterState> {
writeState(state);
try {
return await window.QimingClawBridge?.digital?.saveUiState?.({ state, action, date, metadata }) ?? state;
} catch {
return state;
}
}
async function recordGovernanceCommand(
body: GovernanceCommandRequest,
response: GovernanceCommandResponse,
): Promise<void> {
try {
await window.QimingClawBridge?.digital?.recordGovernanceCommand?.({
commandKind: body.command_kind,
commandId: response.command_id,
correlationId: response.correlation_id,
planId: stringFromUnknown(response.result?.plan_id)
|| stringFromUnknown(body.context_refs?.plan_id)
|| stringFromUnknown(body.payload?.plan_id)
|| null,
scheduleId: stringFromUnknown(response.result?.schedule_id)
|| stringFromUnknown(body.context_refs?.schedule_id)
|| stringFromUnknown(body.payload?.schedule_id)
|| null,
accepted: response.accepted,
message: response.message,
error: response.error ?? null,
payload: body.payload ?? null,
result: response.result ?? null,
});
} catch {
// Bridge persistence is best-effort; the adapter response should not block UI flow.
}
}
async function readCronSettings(): Promise<QimingclawCronSettings> {
try {
return await window.QimingClawBridge?.digital?.getCronSettings?.() ?? defaultCronSettings();
} catch {
return defaultCronSettings();
}
}
async function saveCronSettings(patch: Partial<QimingclawCronSettings>): Promise<QimingclawCronSettings> {
try {
return await window.QimingClawBridge?.digital?.saveCronSettings?.(patch) ?? { ...defaultCronSettings(), ...patch };
} catch {
return { ...defaultCronSettings(), ...patch };
}
}
function defaultCronSettings(): QimingclawCronSettings {
return {
enabled: true,
catch_up_on_startup: true,
max_run_history: 100,
max_catch_up_runs: 5,
max_concurrent_schedule_runs: 1,
};
}
async function handleCronCreate(
body: Record<string, unknown>,
snapshot: QimingclawSnapshot | null,
): Promise<{ status: string; job: CronJob }> {
const stamp = Date.now();
const scheduleId = stringFromUnknown(body.id) || stringFromUnknown(body.schedule_id) || `cron-${slugifyIdPart(stringFromUnknown(body.name) || 'job')}-${stamp}`;
await handleGovernanceCommand({
command_kind: 'create_schedule',
context_refs: { schedule_id: scheduleId, plan_id: stringFromUnknown(body.plan_id) || stringFromUnknown(body.template_id) || undefined },
payload: {
...body,
schedule_id: scheduleId,
cron_expr: stringFromUnknown(body.schedule) || stringFromUnknown(body.cron_expr),
},
correlation_id: `qimingclaw-cron-create-${stamp}`,
idempotency_key: `qimingclaw-cron-create-${scheduleId}-${stamp}`,
}, snapshot);
const refreshed = await readQimingclawSnapshot();
return { status: 'created', job: buildSchedulerJobs(refreshed).find((job) => job.id === scheduleId) ?? cronJobFromBody(scheduleId, body) };
}
async function handleCronPatch(
scheduleId: string,
patch: Record<string, unknown>,
snapshot: QimingclawSnapshot | null,
): Promise<{ status: string; job: CronJob }> {
await handleCronCommand('update_schedule', scheduleId, patch, snapshot);
const refreshed = await readQimingclawSnapshot();
return { status: 'updated', job: buildSchedulerJobs(refreshed).find((job) => job.id === scheduleId) ?? cronJobFromBody(scheduleId, patch) };
}
async function handleCronCommand(
commandKind: GovernanceCommandRequest['command_kind'],
scheduleId: string,
payload: Record<string, unknown>,
snapshot: QimingclawSnapshot | null,
): Promise<GovernanceCommandResponse> {
const stamp = Date.now();
return handleGovernanceCommand({
command_kind: commandKind,
context_refs: { schedule_id: scheduleId, plan_id: stringFromUnknown(payload.plan_id) || stringFromUnknown(payload.template_id) || undefined },
payload: {
...payload,
schedule_id: scheduleId,
cron_expr: stringFromUnknown(payload.schedule) || stringFromUnknown(payload.cron_expr),
},
correlation_id: `qimingclaw-cron-${commandKind}-${stamp}`,
idempotency_key: `qimingclaw-cron-${commandKind}-${scheduleId}-${stamp}`,
}, snapshot);
}
function cronJobFromBody(id: string, body: Record<string, unknown>): CronJob {
const now = new Date().toISOString();
const expression = stringFromUnknown(body.schedule) || stringFromUnknown(body.cron_expr) || '0 9 * * *';
return {
id,
name: stringFromUnknown(body.name) || id,
expression,
command: stringFromUnknown(body.command) || `digital-employee:${id}`,
prompt: stringFromUnknown(body.prompt) || null,
job_type: stringFromUnknown(body.job_type) || 'qimingclaw_schedule',
schedule: { expression, schedule_id: id },
enabled: body.enabled !== false,
delivery: { mode: 'qimingclaw_schedule', schedule_id: id },
template_id: stringFromUnknown(body.template_id) || stringFromUnknown(body.plan_id) || id,
trigger_kind: 'scheduled',
delete_after_run: body.delete_after_run === true,
created_at: now,
next_run: now,
last_run: null,
last_status: null,
last_output: null,
};
}
async function readCronRuns(scheduleId: string, limit: number): Promise<CronRun[]> {
const runs = await window.QimingClawBridge?.digital?.getScheduleRuns?.(scheduleId, limit).catch(() => []) ?? [];
return runs.map((run, index) => ({
id: Number.isFinite(Date.parse(run.createdAt)) ? Date.parse(run.createdAt) + index : index + 1,
job_id: run.scheduleId,
started_at: run.startedAt || run.createdAt,
finished_at: run.finishedAt || '',
status: run.status,
output: run.error || null,
duration_ms: run.startedAt && run.finishedAt
? Math.max(0, Date.parse(run.finishedAt) - Date.parse(run.startedAt))
: null,
}));
}
function parseJsonBody<T>(body: BodyInit | null | undefined): T {
if (typeof body !== 'string' || !body.trim()) return {} as T;
return JSON.parse(body) as T;
}
function todayInputDate(): string {
const date = new Date();
return [
date.getFullYear(),
String(date.getMonth() + 1).padStart(2, '0'),
String(date.getDate()).padStart(2, '0'),
].join('-');
}
function todayAt(hour: number, minute: number): string {
const date = new Date();
date.setHours(hour, minute, 0, 0);
return date.toISOString();
}
function displayDate(date: string): string {
return date.split('-').join('/');
}
function formatTime(value?: string | null): string {
if (!value) return '未知';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return new Intl.DateTimeFormat('zh-CN', {
hour: '2-digit',
minute: '2-digit',
}).format(date);
}
function selectedSetForDate(state: AdapterState, date: string): Set<string> {
return new Set(state.selectedDutyIdsByDate[date] ?? []);
}
function planStatusTone(status: string): string {
if (['completed', 'succeeded', 'success'].includes(status)) return 'success';
if (['running', 'active'].includes(status)) return 'running';
if (['failed', 'error'].includes(status)) return 'danger';
return 'waiting';
}
function isRunning(snapshot: QimingclawSnapshot | null, key: string): boolean {
return snapshot?.services[key]?.running === true;
}
function serviceError(snapshot: QimingclawSnapshot | null, key: string): string | undefined {
return snapshot?.services[key]?.error;
}
function runtimeStatusForPlan(plan: DebugPlan, snapshot: QimingclawSnapshot | null): string {
if (!snapshot) return plan.status;
if (plan.plan_id === 'qimingclaw-client-readiness') {
if (isRunning(snapshot, 'agent') && isRunning(snapshot, 'mcp')) return 'running';
if (serviceError(snapshot, 'agent') || serviceError(snapshot, 'mcp')) return 'failed';
return 'pending';
}
if (plan.plan_id === 'qimingclaw-remote-task-ingress') {
if (isRunning(snapshot, 'computerServer') || isRunning(snapshot, 'lanproxy') || isRunning(snapshot, 'fileServer')) return 'running';
if (serviceError(snapshot, 'computerServer') || serviceError(snapshot, 'lanproxy') || serviceError(snapshot, 'fileServer')) return 'failed';
return 'pending';
}
return plan.status;
}
function serviceLine(snapshot: QimingclawSnapshot | null, key: string, label: string): string {
const service = snapshot?.services[key];
if (!service) return `${label}:未连接`;
if (service.running) {
const suffix = service.port ? `(端口 ${service.port}` : '';
return `${label}:运行中${suffix}`;
}
return `${label}:未运行${service.error ? `${service.error}` : ''}`;
}
function buildManagedServices(snapshot: QimingclawSnapshot | null): DigitalEmployeeManagedService[] {
const fromSnapshot = snapshot?.managedServices ?? [];
if (fromSnapshot.length > 0) {
return fromSnapshot.map((service) => ({
service_id: service.serviceId,
name: service.name,
kind: service.kind,
status: service.status,
status_label: service.statusLabel,
running: service.running,
error: service.error ?? null,
requires_human_action: service.requiresHumanAction,
dependent_tasks: service.dependentTasks ?? [],
restart_count: service.restartCount ?? 0,
last_seen_at: service.lastSeenAt,
instance: service.instance,
lease: service.lease,
}));
}
return [
fallbackManagedService(snapshot, 'agent', 'Agent', 'agent', ['ACP 会话执行', 'Plan Agent']),
fallbackManagedService(snapshot, 'mcp', 'MCP Proxy', 'mcp', ['Skill Catalog', 'MCP 工具调用']),
fallbackManagedService(snapshot, 'computerServer', 'Computer Server', 'desktop', ['/computer/chat', '远程任务接入']),
fallbackManagedService(snapshot, 'fileServer', 'File Server', 'file', ['Artifact 上传', '文件交付']),
fallbackManagedService(snapshot, 'lanproxy', 'Lanproxy', 'network', ['远程访问', '管理端回连']),
fallbackManagedService(snapshot, 'guiServer', 'GUI Agent Server', 'gui', ['GUI 自动化']),
];
}
function fallbackManagedService(
snapshot: QimingclawSnapshot | null,
key: string,
name: string,
kind: string,
dependentTasks: string[],
): DigitalEmployeeManagedService {
const service = snapshot?.services[key];
const running = service?.running === true;
const status = service?.error ? 'degraded' : running ? 'running' : 'stopped';
return {
service_id: key,
name,
kind,
status,
status_label: status === 'running' ? '运行中' : status === 'degraded' ? '异常' : '未运行',
running,
error: service?.error ?? null,
requires_human_action: Boolean(service?.error),
dependent_tasks: dependentTasks,
restart_count: 0,
last_seen_at: snapshot?.generatedAt ?? new Date().toISOString(),
instance: service ? {
port: service.port,
engineType: service.engineType,
serverCount: service.serverCount,
serverNames: service.serverNames,
} : undefined,
lease: {
state: status === 'running' ? 'active' : status,
updatedAt: snapshot?.generatedAt ?? new Date().toISOString(),
},
};
}
function managedServiceLine(service: DigitalEmployeeManagedService): string {
const deps = service.dependent_tasks.length > 0 ? `;依赖任务:${service.dependent_tasks.slice(0, 2).join('、')}` : '';
return `${service.name}${service.status_label}${service.error ? `${service.error}` : ''}${deps}`;
}
function managedServiceSummary(services: DigitalEmployeeManagedService[]): string {
const runningCount = services.filter((service) => service.running).length;
const issueCount = services.filter((service) => service.requires_human_action || service.status === 'degraded').length;
return `ManagedService${runningCount}/${services.length} 运行,${issueCount} 项需处理`;
}
function syncLine(snapshot: QimingclawSnapshot | null): string {
const sync = snapshot?.sync;
if (!sync) return '管理端同步:未连接';
if (!sync.enabled) return '管理端同步:已停用';
if (!sync.endpoint) return '管理端同步:未配置地址';
if (sync.syncing) return `管理端同步:同步中,待同步 ${sync.pending}`;
if (sync.failed > 0) return `管理端同步:${sync.failed} 条失败,待同步 ${sync.pending}`;
if (sync.pending > 0) return `管理端同步:待同步 ${sync.pending}`;
return sync.lastSyncAt ? `管理端同步:已同步,最近 ${sync.lastSyncAt}` : '管理端同步:等待本地数据';
}
function compactSyncText(value?: string | null): string {
return String(value || '').replace(/\s+/g, ' ').trim();
}
function runtimePlans(snapshot: QimingclawSnapshot | null): QimingclawPlanRecord[] {
return snapshot?.runtime?.plans ?? [];
}
function runtimePlanSteps(snapshot: QimingclawSnapshot | null): QimingclawPlanStepRecord[] {
return snapshot?.runtime?.planSteps ?? [];
}
function runtimeTasks(snapshot: QimingclawSnapshot | null): QimingclawTaskRecord[] {
return snapshot?.runtime?.tasks ?? [];
}
function runtimeRuns(snapshot: QimingclawSnapshot | null): QimingclawRunRecord[] {
return snapshot?.runtime?.runs ?? [];
}
function runtimeEvents(snapshot: QimingclawSnapshot | null): QimingclawEventRecord[] {
return snapshot?.runtime?.events ?? [];
}
function runtimeArtifacts(snapshot: QimingclawSnapshot | null): QimingclawArtifactRecord[] {
return snapshot?.runtime?.artifacts ?? [];
}
function runtimeApprovals(snapshot: QimingclawSnapshot | null): QimingclawApprovalRecord[] {
return snapshot?.runtime?.approvals ?? [];
}
function hasRuntimeRecords(snapshot: QimingclawSnapshot | null): boolean {
return runtimePlans(snapshot).length > 0
|| runtimePlanSteps(snapshot).length > 0
|| runtimeTasks(snapshot).length > 0
|| runtimeArtifacts(snapshot).length > 0
|| runtimeApprovals(snapshot).length > 0;
}
function latestRuntimeTime(plan: QimingclawPlanRecord, tasks: QimingclawTaskRecord[]): string {
const sorted = [plan.updatedAt, ...tasks.map((task) => task.updatedAt)]
.filter(Boolean)
.sort();
return sorted[sorted.length - 1] ?? plan.updatedAt ?? plan.createdAt;
}
function taskStatusTone(status: string): 'running' | 'success' | 'danger' | 'waiting' {
const normalized = status.toLowerCase();
if (['completed', 'succeeded', 'success', 'done'].includes(normalized)) return 'success';
if (['failed', 'error', 'rejected'].includes(normalized)) return 'danger';
if (['running', 'active', 'leased', 'syncing'].includes(normalized)) return 'running';
return 'waiting';
}
function taskStatusLabel(status: string): string {
const tone = taskStatusTone(status);
if (tone === 'success') return '已完成';
if (tone === 'danger') return '异常';
if (tone === 'running') return '执行中';
return '待执行';
}
function runtimePlanTasks(snapshot: QimingclawSnapshot | null, planId: string): QimingclawTaskRecord[] {
return runtimeTasks(snapshot).filter((task) => task.planId === planId);
}
function runtimeStepsForPlan(snapshot: QimingclawSnapshot | null, planId: string): QimingclawPlanStepRecord[] {
return runtimePlanSteps(snapshot)
.filter((step) => step.planId === planId)
.sort((left, right) => left.seq - right.seq || left.createdAt.localeCompare(right.createdAt));
}
function taskStepId(task: QimingclawTaskRecord): string {
const record = asRecord(task.payload);
return stringValue(record?.stepId ?? record?.step_id) || (task.planId ? `${task.planId}:runtime` : 'qimingclaw-runtime');
}
function planRecordStatus(plan: QimingclawPlanRecord, tasks: QimingclawTaskRecord[]): string {
if (tasks.some((task) => taskStatusTone(task.status) === 'danger')) return 'failed';
if (tasks.some((task) => taskStatusTone(task.status) === 'running')) return 'running';
if (tasks.length > 0 && tasks.every((task) => taskStatusTone(task.status) === 'success')) return 'completed';
return plan.status || 'pending';
}
function buildPlanAgents(snapshot: QimingclawSnapshot | null): DigitalEmployeePlanAgentState[] {
return runtimePlans(snapshot).map((plan) => {
const tasks = runtimePlanTasks(snapshot, plan.id);
const status = planRecordStatus(plan, tasks);
const tone = planStatusTone(status);
const failedTaskCount = tasks.filter((task) => taskStatusTone(task.status) === 'danger').length;
const runningTaskCount = tasks.filter((task) => taskStatusTone(task.status) === 'running').length;
return {
agent_id: `plan-agent-${slugifyIdPart(plan.id)}`,
plan_id: plan.id,
title: plan.title,
lifecycle: status,
status_label: planStatusLabel(status),
status_tone: tone,
managed_task_count: tasks.length,
running_task_count: runningTaskCount,
failed_task_count: failedTaskCount,
next_action: failedTaskCount > 0 ? 'review_failures' : runningTaskCount > 0 ? 'monitor_runs' : tone === 'success' ? 'archive_results' : 'activate_plan',
can_replan: failedTaskCount > 0,
can_pause: runningTaskCount > 0,
can_resume: status === 'paused',
updated_at: latestRuntimeTime(plan, tasks),
};
});
}
function buildTaskAgents(snapshot: QimingclawSnapshot | null): DigitalEmployeeTaskAgentState[] {
const runs = runtimeRuns(snapshot);
return runtimeTasks(snapshot).map((task) => {
const latestRun = latestRunForTask(runs, task.id);
const tone = taskStatusTone(task.status);
return {
agent_id: `task-agent-${slugifyIdPart(task.id)}`,
task_id: task.id,
plan_id: task.planId ?? null,
title: task.title,
execution_state: task.status,
status_label: taskStatusLabel(task.status),
status_tone: tone,
assigned_skill_id: task.assignedSkillId ?? null,
latest_run_id: latestRun?.id ?? null,
can_retry: tone === 'danger',
can_skip: tone === 'danger' || tone === 'waiting',
can_cancel: tone === 'running',
next_action: tone === 'danger' ? 'retry_or_skip' : tone === 'running' ? 'watch_progress' : tone === 'success' ? 'collect_artifacts' : 'start_run',
updated_at: latestRun?.updatedAt ?? task.updatedAt,
};
});
}
function latestRunForTask(runs: QimingclawRunRecord[], taskId: string): QimingclawRunRecord | null {
const sorted = runs
.filter((run) => run.taskId === taskId)
.sort((left, right) => (right.updatedAt || right.createdAt).localeCompare(left.updatedAt || left.createdAt));
return sorted[0] ?? null;
}
function runtimePlanToDebugPlan(plan: QimingclawPlanRecord, snapshot: QimingclawSnapshot | null): DebugPlan {
const tasks = runtimePlanTasks(snapshot, plan.id);
const planSteps = runtimeStepsForPlan(snapshot, plan.id);
const status = planRecordStatus(plan, tasks);
const knownStepIds = new Set(planSteps.map((step) => step.id));
const orphanTasks = tasks.filter((task) => !knownStepIds.has(taskStepId(task)));
const steps = planSteps.length > 0
? [
...planSteps.map((step) => runtimePlanStepToSummary(step, tasks)),
...(orphanTasks.length > 0 ? [runtimeFallbackStepToSummary(plan.id, orphanTasks)] : []),
]
: [runtimeFallbackStepToSummary(plan.id, tasks)];
return {
plan_id: plan.id,
title: plan.title,
objective: plan.objective ?? undefined,
status,
created_at: plan.createdAt,
updated_at: latestRuntimeTime(plan, tasks),
source: plan.source,
sync_status: plan.syncStatus,
steps,
};
}
function runtimePlanStepToSummary(
step: QimingclawPlanStepRecord,
tasks: QimingclawTaskRecord[],
): PlanStepSummary {
return {
step_id: step.id,
title: step.title,
depends_on: step.dependsOn,
preferred_skills: step.preferredSkillIds,
tasks: tasks
.filter((task) => taskStepId(task) === step.id)
.map((task) => ({
task_id: task.id,
title: task.title,
status: task.status,
})),
};
}
function runtimeFallbackStepToSummary(
planId: string,
tasks: QimingclawTaskRecord[],
): PlanStepSummary {
return {
step_id: `${planId}:runtime`,
title: 'qimingclaw 执行记录',
depends_on: [],
preferred_skills: [...new Set(tasks.map((task) => task.assignedSkillId).filter((value): value is string => Boolean(value)))],
tasks: tasks.map((task) => ({
task_id: task.id,
title: task.title,
status: task.status,
})),
};
}
function runtimeTaskToDebugTask(task: QimingclawTaskRecord): DebugTask {
return {
task_id: task.id,
title: task.title,
status: task.status,
plan_id: task.planId ?? null,
step_id: taskStepId(task),
assigned_skill_id: task.assignedSkillId ?? '',
description: task.syncError ?? '',
created_at: task.createdAt,
updated_at: task.updatedAt,
started_at: task.createdAt,
finished_at: taskStatusTone(task.status) === 'success' || taskStatusTone(task.status) === 'danger'
? task.updatedAt
: null,
error_message: task.syncError ?? null,
sync_status: task.syncStatus,
};
}
function runtimeRunToDebugRun(run: QimingclawRunRecord, events: QimingclawEventRecord[]): DebugRun {
return {
run_id: run.id,
task_id: run.taskId ?? '',
plan_id: run.planId ?? null,
skill_id: undefined,
attempt_no: 1,
lifecycle: taskStatusTone(run.status) === 'success'
? 'Succeeded'
: taskStatusTone(run.status) === 'danger'
? 'Failed'
: taskStatusTone(run.status) === 'running'
? 'Running'
: run.status,
created_at: run.startedAt ?? run.createdAt,
completed_at: run.finishedAt ?? null,
events: events.filter((event) => event.runId === run.id).map((event) => ({
event_id: event.id,
kind: event.kind,
occurred_at: event.occurredAt,
message: event.message,
})),
artifacts: artifactsForRun(run, events),
sync_status: run.syncStatus,
};
}
function syncFailureLine(failure: QimingclawSyncFailure): string {
const error = compactSyncText(failure.error);
const detail = error.length > 120 ? `${error.slice(0, 120)}...` : error;
const retryText = failure.dueForRetry
? '等待补偿'
: failure.nextRetryAt
? `下次重试 ${failure.nextRetryAt}`
: '等待重试';
return `${failure.entityType}/${failure.operation} ${failure.id} 重试 ${failure.attempts} 次,${retryText}${detail ? `${detail}` : ''}`;
}
function withRuntimePlan(plan: DebugPlan, snapshot: QimingclawSnapshot | null): DebugPlan {
return { ...plan, status: runtimeStatusForPlan(plan, snapshot) };
}
function planStatusLabel(status: string): string {
if (status === 'running') return '执行中';
if (status === 'completed' || status === 'succeeded') return '已完成';
if (status === 'failed') return '异常';
return '待执行';
}
function runtimeDebugPlans(snapshot: QimingclawSnapshot | null): DebugPlan[] {
const plans = runtimePlans(snapshot);
if (plans.length > 0) return plans.map((plan) => runtimePlanToDebugPlan(plan, snapshot));
const tasks = runtimeTasks(snapshot);
if (tasks.length === 0) return [];
return tasks.map((task) => ({
plan_id: task.id,
title: task.title,
objective: '未绑定 Plan 的 qimingclaw 本地任务记录。',
status: task.status,
created_at: task.createdAt,
updated_at: task.updatedAt,
source: 'qimingclaw',
sync_status: task.syncStatus,
steps: [
{
step_id: `${task.id}:runtime`,
title: 'qimingclaw 执行记录',
depends_on: [],
preferred_skills: task.assignedSkillId ? [task.assignedSkillId] : [],
tasks: [{ task_id: task.id, title: task.title, status: task.status }],
},
],
}));
}
function templatePlans(snapshot: QimingclawSnapshot | null): DebugPlan[] {
const customTemplates = snapshot?.planTemplates ?? [];
const byId = new Map<string, DebugPlan>();
for (const plan of [...customTemplates, ...PLANS]) {
if (!byId.has(plan.plan_id)) byId.set(plan.plan_id, plan);
}
return Array.from(byId.values());
}
function mergedDebugPlans(snapshot: QimingclawSnapshot | null): DebugPlan[] {
const byId = new Map<string, DebugPlan>();
for (const plan of [...runtimeDebugPlans(snapshot), ...templatePlans(snapshot).map((plan) => withRuntimePlan(plan, snapshot))]) {
if (!byId.has(plan.plan_id)) byId.set(plan.plan_id, plan);
}
return Array.from(byId.values());
}
function planTemplateJob(plan: DebugPlan, snapshot: QimingclawSnapshot | null): CronJob {
const normalizedPlan = withRuntimePlan(plan, snapshot);
const schedule = PLAN_SCHEDULES[plan.plan_id] ?? PLAN_SCHEDULES['qimingclaw-client-readiness']!;
return {
id: `schedule-${plan.plan_id}`,
name: plan.title,
expression: schedule.expression,
command: `digital-employee:${plan.plan_id}`,
prompt: plan.objective ?? null,
job_type: 'plan_template',
schedule: { expression: schedule.expression, template_id: plan.plan_id },
enabled: true,
delivery: { mode: 'plan_template', template_id: plan.plan_id },
template_id: plan.plan_id,
trigger_kind: 'scheduled',
step_count: plan.steps?.length ?? 0,
skill_ids: [...new Set((plan.steps ?? []).flatMap((step) => step.preferred_skills ?? []))],
delete_after_run: false,
created_at: plan.created_at,
next_run: schedule.nextRun,
last_run: normalizedPlan.status === 'running' ? todayAt(9, 5) : null,
last_status: normalizedPlan.status === 'running' ? 'running' : null,
last_output: null,
};
}
function runtimeScheduleJob(schedule: QimingclawScheduleRecord, snapshot: QimingclawSnapshot | null): CronJob {
const plan = schedule.planId
? mergedDebugPlans(snapshot).find((item) => item.plan_id === schedule.planId)
: null;
const runs = snapshot?.runtime?.scheduleRuns ?? [];
const latestRun = runs.find((run) => run.scheduleId === schedule.id);
return {
id: schedule.id,
name: schedule.name,
expression: schedule.cronExpr,
command: schedule.command || `digital-employee:${schedule.planId || schedule.id}`,
prompt: schedule.prompt ?? schedule.description ?? null,
job_type: 'qimingclaw_schedule',
schedule: {
mode: 'qimingclaw_schedule',
schedule_id: schedule.id,
plan_id: schedule.planId,
expression: schedule.cronExpr,
timezone: schedule.timezone,
status: schedule.status,
failure_count: schedule.failureCount,
last_error: schedule.lastError,
},
enabled: schedule.enabled && schedule.status === 'enabled' && !schedule.deletedAt,
delivery: { mode: 'qimingclaw_schedule', schedule_id: schedule.id, plan_id: schedule.planId },
template_id: schedule.planId ?? schedule.id,
trigger_kind: 'scheduled',
step_count: plan?.steps?.length ?? 0,
skill_ids: [...new Set((plan?.steps ?? []).flatMap((step) => step.preferred_skills ?? []))],
delete_after_run: false,
created_at: schedule.createdAt,
next_run: schedule.nextRunAt || schedule.updatedAt,
last_run: schedule.lastRunAt ?? latestRun?.finishedAt ?? latestRun?.startedAt ?? null,
last_status: latestRun?.status ?? (schedule.lastError ? 'failed' : null),
last_output: latestRun?.error ?? schedule.lastError ?? null,
};
}
function buildSchedulerJobs(snapshot: QimingclawSnapshot | null = null): CronJob[] {
const realScheduleJobs = (snapshot?.runtime?.schedules ?? [])
.filter((schedule) => schedule.status !== 'deleted')
.map((schedule) => runtimeScheduleJob(schedule, snapshot));
const templateJobs = templatePlans(snapshot).map((plan) => planTemplateJob(plan, snapshot));
if (realScheduleJobs.length > 0) {
const realTemplateIds = new Set<string | null | undefined>(realScheduleJobs.map((job) => job.template_id));
return [
...realScheduleJobs,
...templateJobs.filter((job) => !realTemplateIds.has(job.template_id)),
];
}
if (hasRuntimeRecords(snapshot)) {
const runtimeJobs = runtimeDebugPlans(snapshot).map((plan) => ({
id: `runtime-${plan.plan_id}`,
name: plan.title,
expression: '',
command: `qimingclaw-runtime:${plan.plan_id}`,
prompt: plan.objective ?? null,
job_type: 'qimingclaw_runtime',
schedule: { mode: 'runtime_record', plan_id: plan.plan_id },
enabled: true,
delivery: { mode: 'qimingclaw_runtime', plan_id: plan.plan_id },
template_id: plan.plan_id,
trigger_kind: 'runtime_record',
step_count: plan.steps?.length ?? 0,
skill_ids: [...new Set((plan.steps ?? []).flatMap((step) => step.preferred_skills ?? []))],
delete_after_run: false,
created_at: plan.created_at,
next_run: plan.updated_at ?? plan.created_at,
last_run: plan.updated_at ?? plan.created_at,
last_status: plan.status,
last_output: null,
}));
const runtimeIds = new Set<string | null | undefined>(runtimeJobs.map((job) => job.template_id));
return [
...runtimeJobs,
...templateJobs.filter((job) => !runtimeIds.has(job.template_id)),
];
}
return templateJobs;
}
function buildDuties(state: AdapterState, date: string, snapshot: QimingclawSnapshot | null): DigitalEmployeeDuty[] {
const selected = selectedSetForDate(state, date);
if (hasRuntimeRecords(snapshot)) {
const plans = runtimePlans(snapshot);
const tasksWithoutPlan = runtimeTasks(snapshot).filter((task) => !task.planId);
const planDuties = plans.map((plan, index) => {
const tasks = runtimePlanTasks(snapshot, plan.id);
const status = planRecordStatus(plan, tasks);
const successCount = tasks.filter((task) => taskStatusTone(task.status) === 'success').length;
const failureCount = tasks.filter((task) => taskStatusTone(task.status) === 'danger').length;
const selectedByDefault = selected.size === 0 ? index < 3 : selected.has(plan.id);
return {
id: plan.id,
title: plan.title,
summary: plan.objective || 'qimingclaw 本地执行记录已同步到数字员工。',
implemented: true,
selectable: true,
selected: selectedByDefault,
status,
status_label: taskStatusLabel(status),
status_tone: taskStatusTone(status),
schedule_text: tasks.length > 0 ? `最近更新 ${formatTime(latestRuntimeTime(plan, tasks))}` : '等待任务记录',
result_text: tasks.length > 0
? `${tasks.length} 项任务,${successCount} 项完成,${failureCount} 项异常`
: `同步状态:${plan.syncStatus}`,
conversation_id: plan.id,
};
});
const standaloneDuties = tasksWithoutPlan.map((task, index) => ({
id: task.id,
title: task.title,
summary: '未绑定 Plan 的 qimingclaw 本地任务记录。',
implemented: true,
selectable: true,
selected: selected.size === 0 ? planDuties.length + index < 3 : selected.has(task.id),
status: task.status,
status_label: taskStatusLabel(task.status),
status_tone: taskStatusTone(task.status),
schedule_text: `最近更新 ${formatTime(task.updatedAt)}`,
result_text: task.syncError || `同步状态:${task.syncStatus}`,
conversation_id: task.id,
}));
const runtimeDutyIds = new Set([...planDuties, ...standaloneDuties].map((duty) => duty.id));
return [
...planDuties,
...standaloneDuties,
...templateDuties(state, date, snapshot).filter((duty) => !runtimeDutyIds.has(duty.id)),
];
}
return templateDuties(state, date, snapshot);
}
function templateDuties(
state: AdapterState,
date: string,
snapshot: QimingclawSnapshot | null,
): DigitalEmployeeDuty[] {
const selected = selectedSetForDate(state, date);
return templatePlans(snapshot).map((rawPlan) => {
const plan = withRuntimePlan(rawPlan, snapshot);
const schedule = PLAN_SCHEDULES[plan.plan_id] ?? PLAN_SCHEDULES['qimingclaw-client-readiness']!;
return {
id: plan.plan_id,
title: plan.title,
summary: plan.objective ?? '等待 qimingclaw 执行链路补充任务说明。',
implemented: true,
selectable: true,
selected: selected.has(plan.plan_id),
status: plan.status,
status_label: planStatusLabel(plan.status),
status_tone: planStatusTone(plan.status),
schedule_text: schedule.scheduleText,
result_text: planResultText(plan, snapshot),
conversation_id: plan.plan_id,
};
});
}
function planResultText(plan: DebugPlan, snapshot: QimingclawSnapshot | null): string {
if (plan.plan_id === 'qimingclaw-client-readiness') {
return [
serviceLine(snapshot, 'agent', 'Agent'),
serviceLine(snapshot, 'mcp', 'MCP'),
`活跃会话:${snapshot?.sessions.active ?? 0}`,
].join('');
}
if (plan.plan_id === 'qimingclaw-remote-task-ingress') {
return [
serviceLine(snapshot, 'computerServer', 'Computer Server'),
serviceLine(snapshot, 'lanproxy', 'Lanproxy'),
serviceLine(snapshot, 'fileServer', 'File Server'),
].join('');
}
return plan.status === 'running' ? '正在同步客户端运行状态' : '等待任务执行';
}
function buildTasks(planId?: string | null, snapshot: QimingclawSnapshot | null = null): DebugTask[] {
const templateTasks = templatePlans(snapshot)
.filter((plan) => !planId || plan.plan_id === planId)
.map((plan) => withRuntimePlan(plan, snapshot))
.flatMap(templatePlanTasks);
if (hasRuntimeRecords(snapshot)) {
const runtimeResult = runtimeTasks(snapshot)
.filter((task) => !planId || task.planId === planId || task.id === planId)
.map(runtimeTaskToDebugTask);
const runtimeTaskIds = new Set(runtimeResult.map((task) => task.task_id));
return [
...runtimeResult,
...templateTasks.filter((task) => !runtimeTaskIds.has(task.task_id)),
];
}
return templateTasks;
}
function templatePlanTasks(plan: DebugPlan): DebugTask[] {
return (plan.steps ?? []).flatMap((step) => (step.tasks ?? []).map((task) => ({
task_id: task.task_id,
title: task.title,
status: plan.status === 'running' && task.status === 'pending' ? 'running' : task.status,
plan_id: plan.plan_id,
step_id: step.step_id,
assigned_skill_id: step.preferred_skills?.[0] ?? '',
description: `${plan.title} · ${step.title}`,
created_at: plan.created_at,
updated_at: plan.status === 'running' ? new Date().toISOString() : plan.created_at,
started_at: task.status === 'running' ? todayAt(9, 5) : null,
finished_at: task.status === 'completed' ? todayAt(9, 10) : null,
})));
}
function buildRuns(tasks: DebugTask[]): DebugRun[] {
return tasks
.filter((task) => task.status === 'running' || task.status === 'completed')
.map((task, index) => ({
run_id: `qimingclaw-run-${index + 1}`,
task_id: task.task_id,
plan_id: task.plan_id,
skill_id: task.assigned_skill_id,
attempt_no: 1,
lifecycle: task.status === 'running' ? 'Running' : 'Succeeded',
created_at: task.started_at ?? task.created_at,
completed_at: task.finished_at,
events: [
{
event_id: `${task.task_id}-event-start`,
kind: 'RunStarted',
occurred_at: task.started_at ?? task.created_at,
message: `${task.title} 已进入 qimingclaw 执行链路`,
},
],
}));
}
function buildTasksResponse(planId?: string | null, snapshot: QimingclawSnapshot | null = null): { tasks: DebugTask[]; runs: DebugRun[]; total: number; truncated: boolean } {
const tasks = buildTasks(planId, snapshot);
if (hasRuntimeRecords(snapshot)) {
const taskIds = new Set(tasks.map((task) => task.task_id));
const runs = runtimeRuns(snapshot)
.filter((run) => !run.taskId || taskIds.has(run.taskId))
.map((run) => runtimeRunToDebugRun(run, runtimeEvents(snapshot)));
return { tasks, runs, total: tasks.length, truncated: false };
}
return { tasks, runs: buildRuns(tasks), total: tasks.length, truncated: false };
}
function buildDebugStore(snapshot: QimingclawSnapshot | null): DebugStoreResponse {
const plans = filterPlans(null, snapshot);
const tasks = buildTasks(null, snapshot);
const runs = buildTasksResponse(null, snapshot).runs;
const realPlanSteps = runtimePlanSteps(snapshot);
return {
tables: {
canonical_events: buildCanonicalEvents(snapshot, tasks, runs),
tasks,
runs,
plans,
plan_steps: realPlanSteps.length > 0
? realPlanSteps.map((step) => runtimePlanStepToSummary(step, runtimeTasks(snapshot)))
: plans.flatMap((plan) => plan.steps ?? []),
approvals: buildApprovals(snapshot),
artifacts: buildArtifacts(snapshot),
},
};
}
function buildDashboard(snapshot: QimingclawSnapshot | null): DashboardResponse {
const tasks = buildTasks(null, snapshot);
const queued = tasks.filter((task) => taskStatusTone(task.status) === 'waiting').length;
const running = tasks.filter((task) => taskStatusTone(task.status) === 'running').length;
const succeeded = tasks.filter((task) => taskStatusTone(task.status) === 'success').length;
const failed = tasks.filter((task) => taskStatusTone(task.status) === 'danger').length;
const approvals = buildApprovals(snapshot);
return {
runtime: {
active_tasks: running,
failed_tasks: failed,
pending_confirmations: approvals.filter((approval) => approval.status === 'pending').length,
},
tasks: {
total: tasks.length,
queued,
running,
succeeded,
failed,
},
approvals: {
total: approvals.length,
pending: approvals.filter((approval) => approval.status === 'pending').length,
},
};
}
function buildCanonicalEvents(
snapshot: QimingclawSnapshot | null,
tasks: DebugTask[],
runs: DebugRun[],
): CanonicalEvent[] {
const runtimeCanonicalEvents = runtimeEvents(snapshot).map((event) => ({
event_id: event.id,
aggregate_type: event.runId ? 'run' : event.taskId ? 'task' : 'plan',
aggregate_id: event.runId ?? event.taskId ?? event.planId ?? event.id,
event_type: event.kind,
kind: event.kind,
message: event.message,
occurred_at: event.occurredAt,
timestamp: event.occurredAt,
plan_id: event.planId ?? null,
task_id: event.taskId ?? null,
}));
const runEvents = runs.flatMap((run) => (run.events ?? []).map((event) => ({
event_id: event.event_id ?? `${run.run_id}:${event.kind ?? 'event'}:${event.occurred_at ?? run.created_at}`,
aggregate_type: 'run',
aggregate_id: run.run_id,
event_type: event.kind ?? run.lifecycle,
kind: event.kind ?? run.lifecycle,
message: event.message ?? run.lifecycle,
occurred_at: event.occurred_at ?? run.completed_at ?? run.created_at,
timestamp: event.occurred_at ?? run.completed_at ?? run.created_at,
plan_id: run.plan_id ?? null,
task_id: run.task_id || null,
})));
const taskEvents = tasks.map((task) => ({
event_id: `task-status-${task.task_id}`,
aggregate_type: 'task',
aggregate_id: task.task_id,
event_type: 'task_status',
kind: task.status,
message: `${task.title}${taskStatusLabel(task.status)}`,
occurred_at: task.updated_at ?? task.created_at,
timestamp: task.updated_at ?? task.created_at,
plan_id: task.plan_id ?? null,
task_id: task.task_id,
}));
const syncEvents = (snapshot?.sync?.recentFailures ?? []).map((failure) => ({
event_id: `digital-sync-failure-${failure.id}`,
aggregate_type: 'sync_outbox',
aggregate_id: failure.id,
event_type: 'digital_sync_failed',
kind: 'digital_sync_failed',
message: syncFailureLine(failure),
occurred_at: failure.lastAttemptAt ?? failure.updatedAt ?? failure.createdAt,
timestamp: failure.lastAttemptAt ?? failure.updatedAt ?? failure.createdAt,
plan_id: null,
task_id: null,
}));
return dedupeCanonicalEvents([
...runtimeCanonicalEvents,
...runEvents,
...taskEvents,
...syncEvents,
]).sort((left, right) => (right.occurred_at ?? '').localeCompare(left.occurred_at ?? ''));
}
function dedupeCanonicalEvents(events: CanonicalEvent[]): CanonicalEvent[] {
const seen = new Set<string>();
return events.filter((event) => {
if (!event.event_id || seen.has(event.event_id)) return false;
seen.add(event.event_id);
return true;
});
}
function buildApprovals(snapshot: QimingclawSnapshot | null): ApprovalEntry[] {
const runtimeApprovalEntries = runtimeApprovals(snapshot).map((approval) => ({
approval_id: approval.id,
task_id: approval.taskId ?? '',
status: approval.status,
created_at: approval.createdAt,
plan_id: approval.planId ?? null,
run_id: approval.runId ?? null,
title: approval.title,
reversible: approval.status === 'pending',
source: 'qimingclaw-runtime',
sync_status: approval.syncStatus,
sync_error: approval.syncError,
payload: approval.payload,
}));
const failures = snapshot?.sync?.recentFailures ?? [];
const syncReviewEntries = failures
.filter((failure) => failure.dueForRetry === false || /credential|auth|token|unauthor/i.test(failure.error ?? ''))
.slice(0, 20)
.map((failure) => ({
approval_id: `sync-review-${failure.id}`,
task_id: failure.entityId,
status: 'pending',
created_at: failure.createdAt,
title: `同步异常待处理:${failure.entityType}/${failure.entityId}`,
reversible: false,
source: 'qimingclaw-sync',
error: failure.error,
}));
return [...runtimeApprovalEntries, ...syncReviewEntries];
}
function buildWorkdayDecisions(
state: AdapterState,
date: string,
snapshot: QimingclawSnapshot | null,
): DigitalEmployeeDecision[] {
const plansById = new Map(runtimePlans(snapshot).map((plan) => [plan.id, plan]));
const tasksById = new Map(runtimeTasks(snapshot).map((task) => [task.id, task]));
const resultByDecisionId = state.decisionResultsByDate?.[date] ?? {};
return buildApprovals(snapshot).filter((approval) => stringValue(approval.source) === 'qimingclaw-runtime').map((approval) => {
const payload = asRecord(approval.payload);
const decisionId = stringValue(approval.approval_id)
|| `approval-${slugifyIdPart(stringValue(approval.task_id) || stringValue(approval.title) || 'item')}`;
const result = resultByDecisionId[decisionId];
const status = decisionStatus(result, stringValue(approval.status) || 'pending');
const planId = stringValue(approval.plan_id);
const taskId = stringValue(approval.task_id);
const plan = planId ? plansById.get(planId) : null;
const task = taskId ? tasksById.get(taskId) : null;
return {
id: decisionId,
title: stringValue(approval.title) || stringValue(payload?.title) || '待确认事项',
scenario: decisionScenario(approval, payload),
status,
status_label: decisionStatusLabel(status, result),
plan_title: plan?.title || stringValue(payload?.plan_title) || stringValue(approval.plan_id) || 'qimingclaw 本地记录',
task_title: task?.title || stringValue(payload?.task_title) || stringValue(approval.task_id) || '待确认任务',
actions: isDecisionOpenStatus(status)
? [
{ id: 'approved', label: '同意', tone: 'primary' },
{ id: 'rejected', label: '驳回', tone: 'secondary' },
]
: [],
};
});
}
function decisionScenario(approval: ApprovalEntry, payload: Record<string, unknown> | null): string {
return stringValue(payload?.scenario)
|| stringValue(payload?.summary)
|| stringValue(payload?.reason)
|| stringValue(payload?.message)
|| stringValue(approval.error)
|| '来自 qimingclaw 本地运行记录的人工确认项。';
}
function decisionStatus(result: string | undefined, originalStatus: string): string {
const normalizedResult = String(result || '').toLowerCase();
if (['approve', 'approved', 'accept', 'accepted'].includes(normalizedResult)) return 'approved';
if (['reject', 'rejected', 'deny', 'denied'].includes(normalizedResult)) return 'rejected';
if (['dismiss', 'dismissed', 'skip', 'skipped'].includes(normalizedResult)) return 'dismissed';
return originalStatus || 'pending';
}
function decisionStatusLabel(status: string, result?: string): string {
if (result) {
if (status === 'approved') return '已同意';
if (status === 'rejected') return '已驳回';
if (status === 'dismissed') return '已忽略';
return '已处理';
}
if (status === 'approved') return '已同意';
if (status === 'rejected') return '已驳回';
if (status === 'completed') return '已完成';
if (status === 'failed') return '异常';
return '待确认';
}
function isDecisionOpenStatus(status: string): boolean {
return ['pending', 'requested'].includes(String(status || '').toLowerCase());
}
function buildArtifacts(snapshot: QimingclawSnapshot | null): ArtifactEntry[] {
const artifacts = [
...runtimeArtifacts(snapshot).map(artifactFromRuntimeRecord),
...runtimeRuns(snapshot).flatMap((run) => artifactsForRun(run, runtimeEvents(snapshot))),
...runtimeEvents(snapshot).flatMap((event) => artifactsFromPayload(event.id, event.payload, event.occurredAt, {
sourceType: 'event',
sourceEventId: event.id,
sourceLabel: `Event ${event.kind}`,
planId: event.planId,
taskId: event.taskId,
runId: event.runId,
})),
];
return dedupeArtifacts(artifacts);
}
function artifactFromRuntimeRecord(artifact: QimingclawArtifactRecord): ArtifactEntry {
const payload = asRecord(artifact.payload);
return {
artifact_id: artifact.id,
subject_id: artifact.runId ?? artifact.taskId ?? artifact.planId ?? artifact.id,
kind: artifact.kind,
name: artifact.name || stringValue(payload?.name ?? payload?.title) || artifactName(artifact.uri ?? '') || artifact.kind,
uri: artifact.uri || stringValue(payload?.uri ?? payload?.url ?? payload?.path) || undefined,
value: stringValue(payload?.value ?? payload?.summary ?? payload?.text) || artifact.uri || artifact.name || artifact.kind,
plan_id: artifact.planId ?? null,
task_id: artifact.taskId ?? null,
run_id: artifact.runId ?? null,
created_at: artifact.createdAt,
source: 'qimingclaw-runtime',
source_type: 'artifact',
source_label: artifactSourceLabel('artifact', artifact.id),
sync_status: artifact.syncStatus,
sync_error: artifact.syncError,
payload: artifact.payload,
};
}
function artifactsForRun(run: QimingclawRunRecord, events: QimingclawEventRecord[]): ArtifactEntry[] {
return dedupeArtifacts([
...artifactsFromPayload(run.id, run.payload, run.finishedAt ?? run.startedAt ?? run.createdAt, {
sourceType: 'run',
sourceLabel: artifactSourceLabel('run', run.id),
planId: run.planId,
taskId: run.taskId,
runId: run.id,
}),
...events
.filter((event) => event.runId === run.id)
.flatMap((event) => artifactsFromPayload(run.id, event.payload, event.occurredAt, {
sourceType: 'event',
sourceEventId: event.id,
sourceLabel: `Event ${event.kind}`,
planId: event.planId,
taskId: event.taskId,
runId: event.runId,
})),
]);
}
interface ArtifactSourceContext {
sourceType: string;
sourceLabel: string;
sourceEventId?: string | null;
planId?: string | null;
taskId?: string | null;
runId?: string | null;
}
function artifactsFromPayload(
subjectId: string,
payload: unknown,
createdAt?: string | null,
sourceContext?: ArtifactSourceContext,
): ArtifactEntry[] {
const record = asRecord(payload);
if (!record) return typeof payload === 'string' ? [artifactFromValue(subjectId, payload, 0, createdAt, sourceContext)] : [];
const explicitArtifacts = [
...arrayValue(record.artifacts),
...arrayValue(record.outputs),
...arrayValue(record.files),
...arrayValue(record.attachments),
];
const artifacts = explicitArtifacts.map((item, index) => artifactFromValue(subjectId, item, index, createdAt, sourceContext));
for (const key of ['artifact', 'outputPath', 'output_path', 'filePath', 'file_path', 'path', 'uri', 'url']) {
if (record[key]) artifacts.push(artifactFromValue(subjectId, record[key], artifacts.length, createdAt, sourceContext));
}
return artifacts.filter((artifact) => artifact.name || artifact.uri || artifact.value);
}
function artifactFromValue(
subjectId: string,
value: unknown,
index: number,
createdAt?: string | null,
sourceContext?: ArtifactSourceContext,
): ArtifactEntry {
const record = asRecord(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}`;
return {
artifact_id: stringValue(record.artifact_id ?? record.id) || `artifact-${slugifyIdPart(subjectId)}-${index + 1}`,
subject_id: stringValue(record.subject_id) || subjectId,
kind: stringValue(record.kind ?? record.type) || 'artifact',
name,
uri,
value: stringValue(record.value ?? record.summary ?? record.text) || uri,
created_at: (stringValue(record.created_at ?? record.createdAt) || createdAt) ?? undefined,
source: 'qimingclaw-runtime',
source_type: stringValue(record.source_type) || sourceContext?.sourceType || 'payload',
source_event_id: stringValue(record.source_event_id) || sourceContext?.sourceEventId || null,
source_label: stringValue(record.source_label) || sourceContext?.sourceLabel || artifactSourceLabel('payload', subjectId),
plan_id: stringValue(record.plan_id) || sourceContext?.planId || null,
task_id: stringValue(record.task_id) || sourceContext?.taskId || null,
run_id: stringValue(record.run_id) || sourceContext?.runId || null,
};
}
const text = String(value ?? '').trim();
return {
artifact_id: `artifact-${slugifyIdPart(subjectId)}-${index + 1}`,
subject_id: subjectId,
kind: 'artifact',
name: artifactName(text) || `产物 ${index + 1}`,
uri: looksLikeUri(text) ? text : undefined,
value: text,
created_at: createdAt ?? undefined,
source: 'qimingclaw-runtime',
source_type: sourceContext?.sourceType || 'payload',
source_event_id: sourceContext?.sourceEventId || null,
source_label: sourceContext?.sourceLabel || artifactSourceLabel('payload', subjectId),
plan_id: sourceContext?.planId || null,
task_id: sourceContext?.taskId || null,
run_id: sourceContext?.runId || null,
};
}
function artifactSourceLabel(sourceType: string, sourceId?: string | null): string {
const suffix = sourceId ? ` ${sourceId}` : '';
switch (sourceType) {
case 'artifact': return `Artifact${suffix}`;
case 'event': return `Event${suffix}`;
case 'run': return `Run${suffix}`;
case 'task': return `Task${suffix}`;
case 'plan': return `Plan${suffix}`;
default: return `Payload${suffix}`;
}
}
function dedupeArtifacts(artifacts: ArtifactEntry[]): ArtifactEntry[] {
const seen = new Set<string>();
return artifacts.filter((artifact) => {
const key = artifact.artifact_id || `${artifact.subject_id}:${artifact.uri || artifact.value || artifact.name}`;
if (!key || seen.has(key)) return false;
seen.add(key);
return Boolean(artifact.name || artifact.uri || artifact.value);
});
}
function asRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === 'object' && !Array.isArray(value)
? value as Record<string, unknown>
: null;
}
function arrayValue(value: unknown): unknown[] {
return Array.isArray(value) ? value : [];
}
function stringValue(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
function looksLikeUri(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 slugifyIdPart(value: string): string {
const slug = value
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
if (slug) return slug;
let hash = 0;
for (let index = 0; index < value.length; index += 1) {
hash = (hash * 31 + value.charCodeAt(index)) >>> 0;
}
return `item-${hash.toString(36)}`;
}
function filterPlans(query?: string | null, snapshot: QimingclawSnapshot | null = null): DebugPlan[] {
const normalized = (query ?? '').trim().toLowerCase();
const plans = mergedDebugPlans(snapshot);
if (!normalized) return plans;
return plans.filter((plan) => (
plan.title.toLowerCase().includes(normalized)
|| plan.plan_id.toLowerCase().includes(normalized)
|| (plan.objective ?? '').toLowerCase().includes(normalized)
));
}
function buildRuntimeWorkEvents(snapshot: QimingclawSnapshot | null): DigitalEmployeeWorkEvent[] {
const plansById = new Map(runtimePlans(snapshot).map((plan) => [plan.id, plan]));
const tasksById = new Map(runtimeTasks(snapshot).map((task) => [task.id, task]));
const events = runtimeEvents(snapshot).map((event) => {
const task = event.taskId ? tasksById.get(event.taskId) : undefined;
const planId = event.planId ?? task?.planId ?? null;
const plan = planId ? plansById.get(planId) : undefined;
const status = task?.status ?? plan?.status ?? (event.kind.includes('failed') || event.kind.includes('error') ? 'failed' : 'running');
const tone = taskStatusTone(status);
return {
id: event.id,
time: formatTime(event.occurredAt),
plan_title: plan?.title ?? 'qimingclaw 本地执行',
task_title: task?.title ?? event.message,
step_title: event.kind,
detail: event.message || event.kind,
status,
status_label: taskStatusLabel(status),
status_tone: tone,
updated_at: event.occurredAt,
conversation_id: planId ?? task?.id ?? event.id,
};
});
if (events.length > 0) return events;
return runtimeTasks(snapshot).map((task) => {
const plan = task.planId ? plansById.get(task.planId) : undefined;
return {
id: `task-event-${task.id}`,
time: formatTime(task.updatedAt),
plan_title: plan?.title ?? 'qimingclaw 本地任务',
task_title: task.title,
step_title: task.assignedSkillId || 'qimingclaw-runtime',
detail: task.syncError || `${task.title}${taskStatusLabel(task.status)},同步状态 ${task.syncStatus}`,
status: task.status,
status_label: taskStatusLabel(task.status),
status_tone: taskStatusTone(task.status),
updated_at: task.updatedAt,
conversation_id: task.planId ?? task.id,
};
});
}
function buildEvents(duties: DigitalEmployeeDuty[], confirmed: boolean, snapshot: QimingclawSnapshot | null): DigitalEmployeeWorkEvent[] {
if (hasRuntimeRecords(snapshot)) {
const runtimeWorkEvents = buildRuntimeWorkEvents(snapshot);
return appendSyncEvent(runtimeWorkEvents, snapshot);
}
if (!confirmed) {
return [{
id: 'event-await-confirmation',
time: '待确认',
plan_title: '任务设置',
task_title: '等待确认今日任务',
step_title: '选择职责',
detail: '请先确认今天要执行的任务,确认后数字员工会开始执行。',
status: 'pending',
status_label: '待确认',
status_tone: 'waiting',
updated_at: new Date().toISOString(),
conversation_id: null,
}];
}
const selectedDuties = duties.filter((duty) => duty.selected);
const events = selectedDuties.map((duty) => ({
id: `event-${duty.id}`,
time: duty.status === 'running' ? '实时' : '待执行',
plan_title: duty.title,
task_title: duty.title,
step_title: duty.status === 'running' ? '同步 qimingclaw 服务状态' : '等待调度',
detail: duty.status === 'running'
? `${duty.result_text}。快照时间:${snapshot?.generatedAt ?? '未连接 bridge'}`
: '任务已确认,将在后续阶段接入 ACP/MCP 真实执行事件。',
status: duty.status === 'running' ? 'running' : 'pending',
status_label: duty.status === 'running' ? '执行中' : '待执行',
status_tone: duty.status === 'running' ? 'running' : 'waiting',
updated_at: new Date().toISOString(),
conversation_id: duty.conversation_id,
}));
return appendSyncEvent(events, snapshot);
}
function appendSyncEvent(events: DigitalEmployeeWorkEvent[], snapshot: QimingclawSnapshot | null): DigitalEmployeeWorkEvent[] {
const sync = snapshot?.sync;
if (sync && (sync.pending > 0 || sync.failed > 0 || sync.lastSyncAt)) {
const recentFailureText = (sync.recentFailures || [])
.slice(0, 3)
.map(syncFailureLine)
.join('');
return [...events, {
id: 'event-management-sync',
time: sync.syncing ? '实时' : '同步',
plan_title: '管理端联动',
task_title: '同步数字员工本地状态',
step_title: sync.failed > 0 ? '同步补偿' : 'outbox 推送',
detail: [
syncLine(snapshot),
sync.lastSyncError ? `错误:${sync.lastSyncError}` : '',
recentFailureText ? `最近失败:${recentFailureText}` : '',
].filter(Boolean).join('。'),
status: sync.failed > 0 ? 'failed' : sync.syncing || sync.pending > 0 ? 'running' : 'completed',
status_label: sync.failed > 0 ? '异常' : sync.syncing || sync.pending > 0 ? '同步中' : '已同步',
status_tone: sync.failed > 0 ? 'danger' : sync.syncing || sync.pending > 0 ? 'running' : 'success',
updated_at: sync.lastSyncAt ?? new Date().toISOString(),
conversation_id: 'qimingclaw-management-sync',
}];
}
return events;
}
function buildExecutionSummaries(duties: DigitalEmployeeDuty[], events: DigitalEmployeeWorkEvent[]): DigitalEmployeeTaskExecutionSummary[] {
const selectedDuties = duties.some((duty) => duty.selected) ? duties.filter((duty) => duty.selected) : duties;
return selectedDuties.map((duty) => {
const related = events.filter((event) => event.conversation_id === duty.conversation_id);
const running = related.filter((event) => event.status === 'running').length;
const success = related.filter((event) => event.status === 'completed').length;
const failure = related.filter((event) => taskStatusTone(event.status) === 'danger').length;
const waiting = Math.max(0, related.length - running - success - failure);
return {
id: duty.id,
plan_id: duty.id,
title: duty.title,
description: duty.summary,
selected: duty.selected,
total_count: Math.max(related.length, 1),
success_count: success,
failure_count: failure,
running_count: running,
waiting_count: related.length === 0 ? 1 : waiting,
latest_status_label: failure > 0 ? '异常' : running > 0 ? '执行中' : success > 0 ? '已完成' : '待执行',
latest_status_tone: failure > 0 ? 'danger' : running > 0 ? 'running' : success > 0 ? 'success' : 'waiting',
business_summary: duty.result_text,
ai_summary: '当前由 qimingclaw 本地运行记录生成。',
};
});
}
function buildRunDetails(events: DigitalEmployeeWorkEvent[]): DigitalEmployeeRunDetail[] {
return events.map((event) => ({
id: `run-detail-${event.id}`,
time: event.time,
task_name: event.task_title,
step_name: event.step_title,
operation_name: event.plan_title,
status_label: event.status_label,
status_tone: event.status_tone,
business_detail: event.detail,
raw_detail: event.detail,
updated_at: event.updated_at,
conversation_id: event.conversation_id,
}));
}
function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): DigitalEmployeeWorkdayProjection {
const state = snapshot?.uiState ?? readState();
const confirmedAt = state.confirmedDates[date] ?? null;
const confirmed = Boolean(confirmedAt);
const duties = buildDuties(state, date, snapshot);
const events = buildEvents(duties, confirmed, snapshot);
const executionSummaries = buildExecutionSummaries(duties, events);
const generatedAt = state.reportGeneratedAtByDate[date] ?? null;
const selectedCount = duties.filter((duty) => duty.selected).length;
const runningCount = events.filter((event) => event.status === 'running').length;
const successCount = events.filter((event) => taskStatusTone(event.status) === 'success').length;
const failureCount = events.filter((event) => taskStatusTone(event.status) === 'danger').length;
const liveRecordCount = runtimePlans(snapshot).length + runtimeTasks(snapshot).length;
const hasLiveRuntime = hasRuntimeRecords(snapshot);
const sync = snapshot?.sync;
const artifactSources = reportArtifactSources(buildArtifacts(snapshot));
const managedServices = buildManagedServices(snapshot);
const managedServiceIssues = managedServices.filter((service) => service.requires_human_action || service.status === 'degraded');
const runningManagedServiceCount = managedServices.filter((service) => service.running).length;
const planAgents = buildPlanAgents(snapshot);
const taskAgents = buildTaskAgents(snapshot);
const actionableTaskAgents = taskAgents.filter((agent) => agent.can_retry || agent.can_skip || agent.can_cancel);
return {
source: 'live',
date,
employee: {
name: state.profile.name,
title: '数字员工',
major: 'qimingclaw 客户端任务执行',
company: state.profile.company,
position: state.profile.position,
current_status: state.profile.currentStatus,
current_status_label: state.profile.currentStatus === 'resting' ? '休息中' : '工作中',
motto: '承接客户端任务,调度 ACP 与 MCP 能力完成执行。',
},
workday: {
confirmed,
confirmed_at: confirmedAt,
current_status: state.profile.currentStatus,
current_status_label: state.profile.currentStatus === 'resting' ? '休息中' : '工作中',
status_label: hasLiveRuntime
? (failureCount > 0 || managedServiceIssues.length > 0 ? '有异常' : runningCount > 0 ? '执行中' : '已同步')
: confirmed ? '运行中' : '待确认',
status_tone: hasLiveRuntime
? (failureCount > 0 || managedServiceIssues.length > 0 ? 'danger' : runningCount > 0 ? 'running' : 'success')
: confirmed ? 'running' : 'waiting',
summary: hasLiveRuntime
? `当前日期 ${displayDate(date)} 已读取 ${liveRecordCount} 条 qimingclaw 本地运行记录,${managedServiceSummary(managedServices)}`
: confirmed
? `当前日期 ${displayDate(date)} 已确认 ${selectedCount} 项任务。`
: `当前日期 ${displayDate(date)} 待确认今日任务。`,
bubble_text: hasLiveRuntime
? managedServiceIssues.length > 0
? `我发现 ${managedServiceIssues.length} 项常驻服务需要处理,会把影响到的任务标出来。`
: '我已接入 qimingclaw 本地运行记录,会持续展示真实任务、事件和同步状态。'
: confirmed
? '我已接入 qimingclaw 客户端执行链路,正在等待真实 Run/Event 数据接入。'
: '请先确认今天要执行的任务,我会把它们映射到 qimingclaw 执行链路。',
},
overview: {
title: '今日执行',
status_label: hasLiveRuntime ? '本地真实记录' : confirmed ? '真实数据' : '待确认',
status_tone: failureCount > 0 ? 'danger' : hasLiveRuntime || confirmed ? 'success' : 'waiting',
metrics: [
{ label: '已选任务', value: String(selectedCount) },
{ label: '运行记录', value: String(events.length) },
{ label: '执行中', value: String(runningCount) },
{ label: '已完成', value: String(successCount) },
{ label: '异常', value: String(failureCount) },
{ label: '待同步', value: String(sync?.pending ?? 0) },
{ label: '同步异常', value: String(sync?.failed ?? 0) },
{ label: '服务运行', value: `${runningManagedServiceCount}/${managedServices.length}` },
{ label: '需处理服务', value: String(managedServiceIssues.length) },
{ label: 'Plan Agent', value: String(planAgents.length) },
{ label: 'Task Agent', value: String(taskAgents.length) },
{ label: '可治理任务', value: String(actionableTaskAgents.length) },
],
},
managed_services: managedServices,
plan_agents: planAgents,
task_agents: taskAgents,
duties,
events,
execution_summaries: executionSummaries,
run_details: buildRunDetails(events),
decisions: buildWorkdayDecisions(state, date, snapshot),
delivery: {
headline: hasLiveRuntime ? 'qimingclaw 本地记录已接入' : confirmed ? 'qimingclaw 执行链路已接入' : '等待确认任务设置',
summary: hasLiveRuntime
? '数字员工页面正在读取本地 Plan / Task / Run / Event并同步展示管理端 outbox 状态。'
: '当前阶段已将数字员工页面切换到 qimingclaw adapter并开始读取客户端服务状态。',
lines: [
'已吸收 sgRobot 数字员工 UI 和核心展示模型。',
hasLiveRuntime
? `已读取本地记录Plan ${runtimePlans(snapshot).length} / Task ${runtimeTasks(snapshot).length} / Run ${runtimeRuns(snapshot).length} / Event ${runtimeEvents(snapshot).length}`
: '已建立 qimingclaw workday/plans/tasks/skills API adapter。',
serviceLine(snapshot, 'agent', 'Agent'),
serviceLine(snapshot, 'mcp', 'MCP'),
serviceLine(snapshot, 'computerServer', 'Computer Server'),
managedServiceSummary(managedServices),
...managedServiceIssues.slice(0, 3).map(managedServiceLine),
`Plan Agent${planAgents.length} 个计划代理Task Agent${taskAgents.length} 个任务代理。`,
actionableTaskAgents[0]
? `待治理任务:${actionableTaskAgents[0].title},下一步 ${actionableTaskAgents[0].next_action}`
: 'Task Agent暂无需要重试、跳过或取消的任务。',
syncLine(snapshot),
`活跃会话:${snapshot?.sessions.active ?? 0} / ${snapshot?.sessions.total ?? 0}`,
],
artifacts: [
{ name: '数字员工运行摘要', status: generatedAt ? 'ready' : 'pending' },
...artifactSources.slice(0, 4).map((artifact) => ({
name: artifact.name,
status: generatedAt ? 'ready' : 'available',
uri: artifact.uri ?? null,
source_label: artifact.source_label ?? null,
source_type: artifact.source_type ?? null,
})),
],
},
daily_report: {
date,
title: `${displayDate(date)} 飞天数字员工日报`,
report_schedule_time: state.reportScheduleTime,
status: generatedAt ? 'ready' : 'not_generated',
generated_at: generatedAt,
generated_by: generatedAt ? 'ai' : 'fallback',
model: 'qimingclaw-adapter',
source: 'qimingclaw',
summary: generatedAt
? `今日已完成数字员工页面到 qimingclaw adapter 的接入,并读取了 qimingclaw 服务状态。${syncLine(snapshot)}`
: '日报将在生成后汇总任务选择、运行明细和后续接入建议。',
totals: {
task_count: selectedCount,
execution_count: events.length,
success_count: successCount,
failure_count: failureCount,
running_count: runningCount,
},
task_sections: executionSummaries,
detail_lines: events.map((event) => `${event.time} ${event.task_title}${event.detail}`),
artifact_sources: artifactSources,
pdf_available: Boolean(generatedAt),
pdf_url: null,
},
};
}
function reportArtifactSources(artifacts: ArtifactEntry[]) {
return artifacts.slice(0, 20).map((artifact) => ({
id: stringValue(artifact.artifact_id) || `${stringValue(artifact.subject_id)}:${stringValue(artifact.uri ?? artifact.name)}`,
name: stringValue(artifact.name) || stringValue(artifact.uri) || stringValue(artifact.value) || '未命名产物',
kind: stringValue(artifact.kind) || null,
uri: stringValue(artifact.uri) || null,
source_label: stringValue(artifact.source_label) || null,
source_type: stringValue(artifact.source_type) || null,
source_event_id: stringValue(artifact.source_event_id) || null,
plan_id: stringValue(artifact.plan_id) || null,
task_id: stringValue(artifact.task_id) || null,
run_id: stringValue(artifact.run_id) || null,
created_at: stringValue(artifact.created_at) || null,
}));
}
async function handleWorkdayAction(body: DigitalEmployeeWorkdayActionRequest, snapshot: QimingclawSnapshot | null): Promise<DigitalEmployeeWorkdayProjection> {
const state = snapshot?.uiState ?? await readBridgeUiState() ?? readState();
const date = body.date || todayInputDate();
if (body.action === 'confirm_workday') {
state.selectedDutyIdsByDate[date] = body.duty_ids ?? [];
state.confirmedDates[date] = new Date().toISOString();
}
if (body.action === 'update_profile') {
state.profile = {
name: body.name || state.profile.name,
company: body.company || state.profile.company,
position: body.position || state.profile.position,
currentStatus: body.current_status || state.profile.currentStatus,
};
}
if (body.action === 'update_report_schedule') {
state.reportScheduleTime = body.report_schedule_time || state.reportScheduleTime;
}
if (body.action === 'generate_daily_report') {
if (body.duty_ids) state.selectedDutyIdsByDate[date] = body.duty_ids;
if (body.report_schedule_time) state.reportScheduleTime = body.report_schedule_time;
state.reportGeneratedAtByDate[date] = new Date().toISOString();
}
if (body.action === 'decide_approval' && body.decision_id) {
state.decisionResultsByDate = state.decisionResultsByDate ?? {};
state.decisionResultsByDate[date] = {
...(state.decisionResultsByDate[date] ?? {}),
[body.decision_id]: body.decision || 'resolved',
};
}
const acpPermissionResult = body.action === 'decide_approval'
? await respondAcpPermissionForDecision(body, snapshot)
: null;
const savedState = await saveState(
state,
body.action,
date,
body.action === 'decide_approval'
? {
decision_id: body.decision_id,
decision: body.decision,
acp_permission: acpPermissionResult,
}
: undefined,
);
return buildWorkday(date, { ...(snapshot ?? { generatedAt: new Date().toISOString(), services: {}, sessions: { total: 0, active: 0, items: [] } }), uiState: savedState });
}
async function respondAcpPermissionForDecision(
body: DigitalEmployeeWorkdayActionRequest,
snapshot: QimingclawSnapshot | null,
): Promise<Record<string, unknown> | null> {
if (!body.decision_id || !body.decision) return null;
const approval = findApprovalForDecision(body.decision_id, snapshot);
if (!approval) return null;
const refs = acpPermissionRefs(approval);
if (!refs) return null;
const response = acpResponseForDecision(body.decision);
const respondPermission = window.QimingClawBridge?.digital?.respondPermission;
if (!respondPermission) return null;
try {
const result = await respondPermission(refs.sessionId, refs.permissionId, response);
return {
permission_id: refs.permissionId,
session_id: refs.sessionId,
response,
success: result?.success !== false,
error: result?.error,
};
} catch (error) {
return {
permission_id: refs.permissionId,
session_id: refs.sessionId,
response,
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
function findApprovalForDecision(decisionId: string, snapshot: QimingclawSnapshot | null): ApprovalEntry | null {
return buildApprovals(snapshot).find((approval) => stringValue(approval.approval_id) === decisionId) ?? null;
}
function acpPermissionRefs(approval: ApprovalEntry): { permissionId: string; sessionId: string } | null {
const payload = asRecord(approval.payload);
const nestedPayload = asRecord(payload?.payload);
const permissionId = stringValue(payload?.permissionId)
|| stringValue(payload?.permission_id)
|| stringValue(payload?.id)
|| stringValue(nestedPayload?.permissionId)
|| stringValue(nestedPayload?.permission_id)
|| stringValue(nestedPayload?.id);
const sessionId = stringValue(payload?.sessionId)
|| stringValue(payload?.session_id)
|| stringValue(nestedPayload?.sessionId)
|| stringValue(nestedPayload?.session_id);
const source = stringValue(payload?.source);
if (!permissionId || !sessionId || (source && source !== 'acp-permission')) return null;
return { permissionId, sessionId };
}
function acpResponseForDecision(decision: string): 'once' | 'always' | 'reject' {
const normalized = decision.toLowerCase();
if (normalized.includes('always')) return 'always';
if (normalized.includes('reject') || normalized.includes('deny')) return 'reject';
return 'once';
}
async function handleGovernanceCommand(
body: GovernanceCommandRequest,
snapshot: QimingclawSnapshot | null,
): Promise<GovernanceCommandResponse> {
const commandId = `qimingclaw-digital-${body.command_kind}-${Date.now()}`;
const correlationId = body.correlation_id || commandId;
const planId = stringFromUnknown(body.context_refs?.plan_id)
|| stringFromUnknown(body.payload?.plan_id)
|| stringFromUnknown(body.payload?.original_plan_id);
const taskId = stringFromUnknown(body.context_refs?.task_id)
|| stringFromUnknown(body.payload?.task_id)
|| stringFromUnknown(body.payload?.original_task_id);
const scheduleId = stringFromUnknown(body.context_refs?.schedule_id)
|| stringFromUnknown(body.payload?.schedule_id);
if (body.command_kind === 'create_plan') {
const createdPlanId = `qimingclaw-local-${slugifyIdPart(stringFromUnknown(body.payload?.title) || planId || 'plan')}-${Date.now()}`;
const response = governanceAccepted(commandId, correlationId, '已在 qimingclaw 兼容层创建本地计划草稿。', {
plan_id: createdPlanId,
status: 'draft',
source: 'qimingclaw-adapter',
requested_action: body.payload?.requested_action,
original_plan_id: body.payload?.original_plan_id ?? planId,
});
await recordGovernanceCommand(body, response);
return response;
}
if (body.command_kind === 'activate_plan' || body.command_kind === 'run_schedule_now') {
const targetPlanId = planId || runtimeDebugPlans(snapshot)[0]?.plan_id || templatePlans(snapshot)[0]?.plan_id || 'qimingclaw-client-readiness';
const dispatch = await dispatchPlan(targetPlanId, snapshot, { recordCommand: false });
const response = governanceAccepted(commandId, correlationId, '已映射为 qimingclaw 本地任务激活。', {
plan_id: targetPlanId,
status: 'active',
dispatched_tasks: dispatch.dispatched_tasks,
source: 'qimingclaw-adapter',
});
await recordGovernanceCommand(body, response);
return response;
}
if (body.command_kind === 'create_schedule') {
const response = governanceAccepted(commandId, correlationId, '已记录 qimingclaw 兼容定时任务请求。', {
schedule_id: scheduleId || `qimingclaw-schedule-${Date.now()}`,
plan_id: planId,
status: 'enabled',
source: 'qimingclaw-adapter',
});
await recordGovernanceCommand(body, response);
return response;
}
if (['update_schedule', 'pause_schedule', 'resume_schedule', 'delete_schedule'].includes(body.command_kind)) {
const status = body.command_kind === 'pause_schedule'
? 'paused'
: body.command_kind === 'delete_schedule'
? 'deleted'
: 'enabled';
const response = governanceAccepted(commandId, correlationId, `已接受 ${body.command_kind},并更新 qimingclaw 本地定时任务状态。`, {
schedule_id: scheduleId || `qimingclaw-schedule-${Date.now()}`,
plan_id: planId,
status,
source: 'qimingclaw-adapter',
});
await recordGovernanceCommand(body, response);
return response;
}
if (['submit_plan', 'approve_plan', 'amend_plan', 'resume_plan', 'pause_plan', 'cancel_plan'].includes(body.command_kind)) {
const response = governanceAccepted(commandId, correlationId, `已接受 ${body.command_kind},当前由 qimingclaw 本地 adapter 记录兼容状态。`, {
plan_id: planId,
status: body.command_kind === 'approve_plan'
? 'approved'
: body.command_kind === 'submit_plan'
? 'reviewing'
: 'accepted',
source: 'qimingclaw-adapter',
});
await recordGovernanceCommand(body, response);
return response;
}
if (['retry_task', 'skip_task', 'pause_task', 'resume_task', 'provide_task_input', 'cancel_task', 'start_run', 'cancel_run'].includes(body.command_kind)) {
const targetTask = taskId || runtimeTasks(snapshot)[0]?.id || 'qimingclaw-task-agent';
const response = governanceAccepted(commandId, correlationId, `已接受 ${body.command_kind},并记录到 qimingclaw Task Agent 控制平面。`, {
plan_id: planId || runtimeTasks(snapshot).find((task) => task.id === targetTask)?.planId || 'qimingclaw-task-agent-plan',
task_id: targetTask,
task_agent_id: `task-agent-${slugifyIdPart(targetTask)}`,
status: taskGovernanceStatus(body.command_kind),
next_action: taskGovernanceNextAction(body.command_kind),
source: 'qimingclaw-task-agent',
});
await recordGovernanceCommand(body, response);
return response;
}
const response = {
command_id: commandId,
accepted: false,
correlation_id: correlationId,
error: 'missing_capability',
message: `qimingclaw 数字员工兼容层尚未支持 ${body.command_kind}`,
};
await recordGovernanceCommand(body, response);
return response;
}
function governanceAccepted(
commandId: string,
correlationId: string,
message: string,
result: Record<string, unknown>,
): GovernanceCommandResponse {
return {
command_id: commandId,
accepted: true,
correlation_id: correlationId,
message,
result,
};
}
function taskGovernanceStatus(commandKind: string): string {
switch (commandKind) {
case 'retry_task':
case 'start_run':
case 'resume_task':
return 'running';
case 'skip_task':
return 'skipped';
case 'cancel_task':
case 'cancel_run':
return 'cancelled';
case 'pause_task':
return 'paused';
default:
return 'accepted';
}
}
function taskGovernanceNextAction(commandKind: string): string {
switch (commandKind) {
case 'retry_task':
return 'start_retry_run';
case 'skip_task':
return 'continue_next_task';
case 'pause_task':
return 'await_resume';
case 'resume_task':
return 'watch_progress';
case 'provide_task_input':
return 'resume_with_input';
case 'cancel_task':
case 'cancel_run':
return 'close_execution';
case 'start_run':
return 'watch_progress';
default:
return 'record_control_event';
}
}
function stringFromUnknown(value: unknown): string {
return typeof value === 'string' && value.trim() ? value.trim() : '';
}
function buildPlanFlow(planId: string, snapshot: QimingclawSnapshot | null = null): FlowResponse {
if (hasRuntimeRecords(snapshot)) {
const plan = runtimePlans(snapshot).find((candidate) => candidate.id === planId);
const tasks = buildTasks(planId, snapshot);
const planSteps = runtimeStepsForPlan(snapshot, planId);
const runs = buildTasksResponse(planId, snapshot).runs;
const events = runtimeEvents(snapshot).filter((event) => event.planId === planId || tasks.some((task) => task.task_id === event.taskId));
const title = plan?.title ?? tasks[0]?.title ?? planId;
const objective = plan?.objective ?? 'qimingclaw 本地运行记录';
const qimingclawFlowSteps: FlowResponse['steps'] = [];
let nextSeq = 2;
if (planSteps.length > 0) {
const knownStepIds = new Set(planSteps.map((step) => step.id));
for (const step of planSteps) {
qimingclawFlowSteps.push({
seq: nextSeq++,
lane: 'qimingclaw',
label: step.title,
sub_label: `${taskStatusLabel(step.status)} · 依赖 ${step.dependsOn.length} · 技能 ${step.preferredSkillIds.length}`,
timestamp: step.updatedAt,
color: '#22c55e',
source_type: 'plan_step',
});
for (const task of tasks.filter((candidate) => candidate.step_id === step.id)) {
qimingclawFlowSteps.push(runtimeTaskFlowStep(task, nextSeq++));
}
}
const orphanTasks = tasks.filter((task) => !knownStepIds.has(task.step_id || ''));
if (orphanTasks.length > 0) {
qimingclawFlowSteps.push({
seq: nextSeq++,
lane: 'qimingclaw',
label: '未归属步骤',
sub_label: `包含 ${orphanTasks.length} 个任务`,
timestamp: orphanTasks[0]?.updated_at ?? new Date().toISOString(),
color: '#22c55e',
source_type: 'plan_step',
});
for (const task of orphanTasks) qimingclawFlowSteps.push(runtimeTaskFlowStep(task, nextSeq++));
}
} else {
for (const task of tasks) qimingclawFlowSteps.push(runtimeTaskFlowStep(task, nextSeq++));
}
return {
plan_id: planId,
plan_title: title,
plan_status: plan ? planRecordStatus(plan, runtimePlanTasks(snapshot, plan.id)) : tasks[0]?.status,
plan_objective: objective,
lanes: [
{ key: 'intent', label: '目标', color: '#38bdf8' },
{ key: 'qimingclaw', label: 'qimingclaw', color: '#22c55e' },
{ key: 'execution', label: '执行', color: '#f59e0b' },
],
steps: [
{
seq: 1,
lane: 'intent',
label: title,
sub_label: objective,
timestamp: plan?.createdAt ?? tasks[0]?.created_at ?? new Date().toISOString(),
color: '#38bdf8',
source_type: 'plan',
},
...qimingclawFlowSteps,
...runs.map((run, index) => ({
seq: nextSeq + index,
lane: 'execution',
label: run.lifecycle || 'Run',
sub_label: run.run_id,
timestamp: run.completed_at ?? run.created_at,
color: '#f59e0b',
source_type: 'run',
})),
],
events: events.map((event) => ({
event_id: event.id,
kind: event.kind,
occurred_at: event.occurredAt,
message: event.message,
})),
};
}
const plan = withRuntimePlan(templatePlans(snapshot).find((candidate) => candidate.plan_id === planId) ?? templatePlans(snapshot)[0]!, snapshot);
const timestamp = new Date().toISOString();
return {
plan_id: plan.plan_id,
plan_title: plan.title,
plan_status: plan.status,
plan_objective: plan.objective,
lanes: [
{ key: 'intent', label: '目标', color: '#38bdf8' },
{ key: 'qimingclaw', label: 'qimingclaw', color: '#22c55e' },
{ key: 'execution', label: '执行', color: '#f59e0b' },
],
steps: [
{
seq: 1,
lane: 'intent',
label: plan.title,
sub_label: plan.objective,
timestamp: plan.created_at,
color: '#38bdf8',
source_type: 'plan',
},
{
seq: 2,
lane: 'qimingclaw',
label: '接入客户端能力',
sub_label: 'ACP / MCP / computer server adapter',
timestamp,
color: '#22c55e',
source_type: 'adapter',
},
{
seq: 3,
lane: 'execution',
label: plan.status === 'running' ? '已读取 qimingclaw 服务状态' : '等待调度',
sub_label: planResultText(plan, snapshot),
timestamp,
color: '#f59e0b',
source_type: 'run',
},
],
events: [
{
event_id: `${plan.plan_id}-adapter-event`,
kind: 'qimingclaw_adapter',
occurred_at: timestamp,
message: '数字员工页面已通过 qimingclaw adapter 获取数据。',
},
],
};
}
function runtimeTaskFlowStep(task: DebugTask, seq: number): FlowResponse['steps'][number] {
return {
seq,
lane: 'qimingclaw',
label: task.title,
sub_label: `${taskStatusLabel(task.status)} · ${task.assigned_skill_id || 'qimingclaw-runtime'}`,
timestamp: task.updated_at,
color: '#22c55e',
source_type: 'task',
};
}
function buildTaskFlow(taskId: string, snapshot: QimingclawSnapshot | null = null): FlowResponse {
const task = runtimeTasks(snapshot).find((candidate) => candidate.id === taskId);
if (!task) {
return buildPlanFlow(taskId, snapshot);
}
const plan = task.planId ? runtimePlans(snapshot).find((candidate) => candidate.id === task.planId) : null;
const runs = runtimeRuns(snapshot).filter((run) => run.taskId === task.id);
const events = runtimeEvents(snapshot).filter((event) => event.taskId === task.id || runs.some((run) => run.id === event.runId));
return {
plan_id: task.planId ?? task.id,
plan_title: plan?.title ?? task.title,
plan_status: task.status,
plan_objective: plan?.objective ?? 'qimingclaw 本地任务记录',
lanes: [
{ key: 'task', label: '任务', color: '#38bdf8' },
{ key: 'execution', label: '执行', color: '#22c55e' },
{ key: 'event', label: '事件', color: '#f59e0b' },
],
steps: [
{
seq: 1,
lane: 'task',
label: task.title,
sub_label: `${taskStatusLabel(task.status)} · ${task.assignedSkillId || 'qimingclaw-runtime'}`,
timestamp: task.updatedAt,
color: '#38bdf8',
source_type: 'task',
},
...runs.map((run, index) => ({
seq: index + 2,
lane: 'execution',
label: run.status || 'Run',
sub_label: run.id,
timestamp: run.finishedAt ?? run.startedAt ?? run.createdAt,
color: '#22c55e',
source_type: 'run',
})),
...events.map((event, index) => ({
seq: runs.length + index + 2,
lane: 'event',
label: event.kind,
sub_label: event.message,
timestamp: event.occurredAt,
color: '#f59e0b',
source_type: 'event',
})),
],
events: events.map((event) => ({
event_id: event.id,
kind: event.kind,
occurred_at: event.occurredAt,
message: event.message,
})),
};
}
async function dispatchPlan(
planId: string,
snapshot: QimingclawSnapshot | null = null,
options: { recordCommand?: boolean } = {},
): Promise<PlanDispatchResponse> {
const state = snapshot?.uiState ?? await readBridgeUiState() ?? readState();
const date = todayInputDate();
const plan = mergedDebugPlans(snapshot).find((candidate) => candidate.plan_id === planId);
const activatedAt = Date.now();
const correlationId = `dispatch-${planId}-${activatedAt}`;
const selected = new Set(state.selectedDutyIdsByDate[date] ?? []);
selected.add(planId);
state.selectedDutyIdsByDate[date] = [...selected];
state.confirmedDates[date] = state.confirmedDates[date] ?? new Date().toISOString();
await saveState(state, 'confirm_workday', date);
const dispatchedTasks = buildTasks(planId, snapshot).map((task) => ({
task_id: task.task_id,
title: task.title,
status: task.status === 'pending' ? 'running' : task.status,
}));
if (options.recordCommand !== false) {
await recordGovernanceCommand(
{
command_kind: 'activate_plan',
context_refs: { plan_id: planId },
payload: {
plan_id: planId,
title: plan?.title,
objective: plan?.objective,
requested_action: 'dispatch_plan',
},
correlation_id: correlationId,
},
governanceAccepted(
`qimingclaw-digital-dispatch-${slugifyIdPart(planId)}-${activatedAt}`,
correlationId,
'已将数字员工模板执行写入 qimingclaw 本地运行记录。',
{
plan_id: planId,
status: 'active',
dispatched_tasks: dispatchedTasks,
source: 'qimingclaw-adapter',
},
),
);
}
return {
ok: true,
plan_id: planId,
source_plan_id: planId,
dispatched_tasks: dispatchedTasks,
};
}
function buildPlanMessages(planId: string, snapshot: QimingclawSnapshot | null = null): Array<{ role: string; content: string; created_at?: string | null }> {
const runtimePlan = runtimePlans(snapshot).find((candidate) => candidate.id === planId);
if (runtimePlan) {
const events = runtimeEvents(snapshot).filter((event) => event.planId === planId).slice(0, 12);
return [
{
role: 'user',
content: `查看 qimingclaw 本地任务:${runtimePlan.title}`,
created_at: runtimePlan.createdAt,
},
...events.map((event) => ({
role: 'assistant',
content: `${event.kind}${event.message}`,
created_at: event.occurredAt,
})),
];
}
const plan = templatePlans(snapshot).find((candidate) => candidate.plan_id === planId) ?? templatePlans(snapshot)[0]!;
return [
{
role: 'user',
content: `执行数字员工任务:${plan.title}`,
created_at: plan.created_at,
},
{
role: 'assistant',
content: `已接入 qimingclaw adapter。目标${plan.objective ?? plan.title}`,
created_at: new Date().toISOString(),
},
];
}