2902 lines
108 KiB
TypeScript
2902 lines
108 KiB
TypeScript
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(),
|
||
},
|
||
];
|
||
}
|