diff --git a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/api.ts b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/api.ts index b89c3ad7..4c0ea0ce 100644 --- a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/api.ts +++ b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/api.ts @@ -613,10 +613,6 @@ export function postDigitalEmployeeWorkdayAction( }); } -export function digitalEmployeeReportPdfUrl(date: string): string { - return `${apiOrigin()}${apiBase}/api/digital-employee/reports/${encodeURIComponent(date)}/pdf`; -} - export function dispatchPlanNow(planId: string): Promise { return apiFetch(`/api/debug/plan/${encodeURIComponent(planId)}/dispatch`, { method: 'POST', diff --git a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/consoleDataAdapter.ts b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/consoleDataAdapter.ts index ef3fd9d6..26d795bc 100644 --- a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/consoleDataAdapter.ts +++ b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/consoleDataAdapter.ts @@ -432,6 +432,18 @@ function categoryFor(skillId: string): string { return '综合'; } +function capabilityLevelFor(value: unknown): SkillSummary['capabilityLevel'] { + if ( + value === 'core' || + value === 'standard' || + value === 'specialized' || + value === 'experimental' + ) { + return value; + } + return 'standard'; +} + function zhDescription(skillId: string, rawDescription: unknown, name: string): string { const description = str(rawDescription).trim(); if (description && hasChinese(description)) return description; @@ -450,8 +462,8 @@ export function adaptSkillSummaries(skillsData: Obj[] | null): SkillSummary[] { version: s.version ? str(s.version) : null, enabled: Boolean(s.enabled), description: zhDescription(sid, s.description, zhName(sid, str(s.name, sid))), - category: categoryFor(sid), - capabilityLevel: 'standard' as const, + category: s.category ? str(s.category, categoryFor(sid)) : categoryFor(sid), + capabilityLevel: capabilityLevelFor(s.capability_level ?? s.capabilityLevel), source: 'skill-api' as const, }; }); diff --git a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/qimingclawAdapter.ts b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/qimingclawAdapter.ts index 2c7616e6..253ce326 100644 --- a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/qimingclawAdapter.ts +++ b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/qimingclawAdapter.ts @@ -1,8 +1,14 @@ import type { + ApprovalEntry, + ArtifactEntry, + CanonicalEvent, CronJob, + DashboardResponse, DebugPlan, DebugRun, + DebugStoreResponse, DebugTask, + DigitalEmployeeDecision, DigitalEmployeeDuty, DigitalEmployeeRunDetail, DigitalEmployeeTaskExecutionSummary, @@ -10,6 +16,8 @@ import type { DigitalEmployeeWorkdayActionRequest, DigitalEmployeeWorkdayProjection, FlowResponse, + GovernanceCommandRequest, + GovernanceCommandResponse, PlanDispatchResponse, SkillSpec, } from '@/types/api'; @@ -20,6 +28,10 @@ declare global { QimingClawBridge?: { digital?: { getSnapshot?: () => Promise; + getRuntimeRecords?: () => Promise; + getUiState?: () => Promise; + getSkillCapabilities?: () => Promise; + saveUiState?: (update: { state: AdapterState; action?: string; date?: string; metadata?: Record }) => Promise; getSyncStatus?: () => Promise; flushSync?: () => Promise; }; @@ -35,6 +47,7 @@ interface AdapterState { confirmedDates: Record; reportScheduleTime: string; reportGeneratedAtByDate: Record; + decisionResultsByDate: Record>; profile: { name: string; company: string; @@ -107,6 +120,117 @@ interface QimingclawSnapshot { items: Array<{ id: string; title?: string; status?: string; engineType?: string; projectId?: string }>; }; sync?: QimingclawSyncStatus | null; + runtime?: QimingclawRuntimeRecords | null; + uiState?: AdapterState | null; +} + +interface QimingclawRuntimeRecords { + generatedAt: string; + plans: QimingclawPlanRecord[]; + tasks: QimingclawTaskRecord[]; + runs: QimingclawRunRecord[]; + events: QimingclawEventRecord[]; + artifacts?: QimingclawArtifactRecord[]; + approvals?: QimingclawApprovalRecord[]; +} + +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 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[] = [ @@ -256,10 +380,13 @@ export async function handleQimingclawDigitalApi( return buildWorkday(url.searchParams.get('date') || todayInputDate(), await readQimingclawSnapshot()) as T; } if (method === 'POST' && pathname === '/api/digital-employee/workday/actions') { - return handleWorkdayAction(parseJsonBody(options.body), await readQimingclawSnapshot()) as T; + return await handleWorkdayAction(parseJsonBody(options.body), await readQimingclawSnapshot()) as T; + } + if (method === 'POST' && pathname === '/api/governance/command') { + return await handleGovernanceCommand(parseJsonBody(options.body), await readQimingclawSnapshot()) as T; } if (method === 'GET' && pathname === '/api/scheduler/jobs') { - return { jobs: buildSchedulerJobs() } as T; + return { jobs: buildSchedulerJobs(await readQimingclawSnapshot()) } as T; } if (method === 'GET' && pathname === '/api/debug/plans') { return { plans: filterPlans(url.searchParams.get('q'), await readQimingclawSnapshot()) } as T; @@ -268,17 +395,29 @@ export async function handleQimingclawDigitalApi( 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 dispatchPlan(decodeURIComponent(pathname.split('/')[4] || '')) as T; + 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] || '')) } as T; + return { messages: buildPlanMessages(decodeURIComponent(pathname.split('/')[3] || ''), await readQimingclawSnapshot()) } as T; } if (method === 'GET' && pathname === '/api/skill') { - return { skills: SKILLS } as T; + return { skills: await buildSkills() } as T; } if (method === 'POST' && pathname.startsWith('/api/skill/') && pathname.endsWith('/enabled')) { const skillId = decodeURIComponent(pathname.split('/')[3] || ''); @@ -295,6 +434,7 @@ function defaultState(): AdapterState { confirmedDates: {}, reportScheduleTime: DEFAULT_REPORT_TIME, reportGeneratedAtByDate: {}, + decisionResultsByDate: {}, profile: { name: '飞天数字员工', company: 'qimingclaw 客户端', @@ -321,13 +461,64 @@ async function readQimingclawSnapshot(): Promise { try { const snapshot = await window.QimingClawBridge?.digital?.getSnapshot?.() ?? null; if (!snapshot) return null; - const sync = await window.QimingClawBridge?.digital?.getSyncStatus?.().catch(() => null) ?? null; - return { ...snapshot, sync }; + const [sync, runtime, uiState] = await Promise.all([ + window.QimingClawBridge?.digital?.getSyncStatus?.().catch(() => null) ?? null, + window.QimingClawBridge?.digital?.getRuntimeRecords?.().catch(() => null) ?? null, + readBridgeUiState(), + ]); + return { ...snapshot, sync, runtime, uiState }; } catch { return null; } } +async function readBridgeUiState(): Promise { + try { + return await window.QimingClawBridge?.digital?.getUiState?.() ?? null; + } catch { + return null; + } +} + +async function readBridgeSkillCapabilities(): Promise { + try { + const result = await window.QimingClawBridge?.digital?.getSkillCapabilities?.() ?? null; + return Array.isArray(result?.skills) ? result.skills : []; + } catch { + return []; + } +} + +async function buildSkills(): Promise { + return dedupeSkills([...SKILLS, ...await readBridgeSkillCapabilities()]); +} + +function dedupeSkills(skills: SkillSpec[]): SkillSpec[] { + const seen = new Set(); + 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, +): Promise { + writeState(state); + try { + return await window.QimingClawBridge?.digital?.saveUiState?.({ state, action, date, metadata }) ?? state; + } catch { + return state; + } +} + function parseJsonBody(body: BodyInit | null | undefined): T { if (typeof body !== 'string' || !body.trim()) return {} as T; return JSON.parse(body) as T; @@ -352,6 +543,16 @@ 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 { return new Set(state.selectedDutyIdsByDate[date] ?? []); } @@ -411,6 +612,146 @@ function compactSyncText(value?: string | null): string { return String(value || '').replace(/\s+/g, ' ').trim(); } +function runtimePlans(snapshot: QimingclawSnapshot | null): QimingclawPlanRecord[] { + return snapshot?.runtime?.plans ?? []; +} + +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 + || 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 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 runtimePlanToDebugPlan(plan: QimingclawPlanRecord, snapshot: QimingclawSnapshot | null): DebugPlan { + const tasks = runtimePlanTasks(snapshot, plan.id); + const status = planRecordStatus(plan, 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: [ + { + step_id: `${plan.id}: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: task.planId ? `${task.planId}:runtime` : 'qimingclaw-runtime', + 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; @@ -433,7 +774,56 @@ function planStatusLabel(status: string): string { return '待执行'; } -function buildSchedulerJobs(): CronJob[] { +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 buildSchedulerJobs(snapshot: QimingclawSnapshot | null = null): CronJob[] { + if (hasRuntimeRecords(snapshot)) { + return 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, + })); + } return PLANS.map((plan) => { const schedule = PLAN_SCHEDULES[plan.plan_id] ?? PLAN_SCHEDULES['qimingclaw-client-readiness']!; return { @@ -462,6 +852,48 @@ function buildSchedulerJobs(): CronJob[] { 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, + })); + return [...planDuties, ...standaloneDuties]; + } return PLANS.map((rawPlan) => { const plan = withRuntimePlan(rawPlan, snapshot); const schedule = PLAN_SCHEDULES[plan.plan_id] ?? PLAN_SCHEDULES['qimingclaw-client-readiness']!; @@ -501,6 +933,11 @@ function planResultText(plan: DebugPlan, snapshot: QimingclawSnapshot | null): s } function buildTasks(planId?: string | null, snapshot: QimingclawSnapshot | null = null): DebugTask[] { + if (hasRuntimeRecords(snapshot)) { + return runtimeTasks(snapshot) + .filter((task) => !planId || task.planId === planId || task.id === planId) + .map(runtimeTaskToDebugTask); + } return PLANS .filter((plan) => !planId || plan.plan_id === planId) .map((plan) => withRuntimePlan(plan, snapshot)) @@ -544,12 +981,378 @@ function buildRuns(tasks: DebugTask[]): DebugRun[] { 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; + return { + tables: { + canonical_events: buildCanonicalEvents(snapshot, tasks, runs), + tasks, + runs, + plans, + plan_steps: 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(); + 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).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 | 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)), + ]; + 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', + 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), + ...events + .filter((event) => event.runId === run.id) + .flatMap((event) => artifactsFromPayload(run.id, event.payload, event.occurredAt)), + ]); +} + +function artifactsFromPayload(subjectId: string, payload: unknown, createdAt?: string | null): ArtifactEntry[] { + const record = asRecord(payload); + if (!record) return typeof payload === 'string' ? [artifactFromValue(subjectId, payload, 0, createdAt)] : []; + + 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)); + + 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)); + } + + return artifacts.filter((artifact) => artifact.name || artifact.uri || artifact.value); +} + +function artifactFromValue(subjectId: string, value: unknown, index: number, createdAt?: string | null): 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', + }; + } + + 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', + }; +} + +function dedupeArtifacts(artifacts: ArtifactEntry[]): ArtifactEntry[] { + const seen = new Set(); + 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 | null { + return value && typeof value === 'object' && !Array.isArray(value) + ? value as Record + : 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 = PLANS.map((plan) => withRuntimePlan(plan, snapshot)); + const plans = hasRuntimeRecords(snapshot) + ? runtimeDebugPlans(snapshot) + : PLANS.map((plan) => withRuntimePlan(plan, snapshot)); if (!normalized) return plans; return plans.filter((plan) => ( plan.title.toLowerCase().includes(normalized) @@ -558,7 +1361,54 @@ function filterPlans(query?: string | null, snapshot: QimingclawSnapshot | null )); } +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', @@ -591,13 +1441,17 @@ function buildEvents(duties: DigitalEmployeeDuty[], confirmed: boolean, snapshot 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(';'); - events.push({ + return [...events, { id: 'event-management-sync', time: sync.syncing ? '实时' : '同步', plan_title: '管理端联动', @@ -613,16 +1467,19 @@ function buildEvents(duties: DigitalEmployeeDuty[], confirmed: boolean, snapshot 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[] { - return duties.filter((duty) => duty.selected).map((duty) => { + 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, @@ -631,13 +1488,13 @@ function buildExecutionSummaries(duties: DigitalEmployeeDuty[], events: DigitalE selected: duty.selected, total_count: Math.max(related.length, 1), success_count: success, - failure_count: 0, + failure_count: failure, running_count: running, - waiting_count: running > 0 ? 0 : 1, - latest_status_label: running > 0 ? '执行中' : '待执行', - latest_status_tone: running > 0 ? 'running' : 'waiting', + 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 adapter 生成,后续接入真实执行日志。', + ai_summary: '当前由 qimingclaw 本地运行记录生成。', }; }); } @@ -659,7 +1516,7 @@ function buildRunDetails(events: DigitalEmployeeWorkEvent[]): DigitalEmployeeRun } function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): DigitalEmployeeWorkdayProjection { - const state = readState(); + const state = snapshot?.uiState ?? readState(); const confirmedAt = state.confirmedDates[date] ?? null; const confirmed = Boolean(confirmedAt); const duties = buildDuties(state, date, snapshot); @@ -668,6 +1525,10 @@ function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): Digita 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; return { @@ -688,23 +1549,33 @@ function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): Digita confirmed_at: confirmedAt, current_status: state.profile.currentStatus, current_status_label: state.profile.currentStatus === 'resting' ? '休息中' : '工作中', - status_label: confirmed ? '运行中' : '待确认', - status_tone: confirmed ? 'running' : 'waiting', - summary: confirmed - ? `当前日期 ${displayDate(date)} 已确认 ${selectedCount} 项任务。` - : `当前日期 ${displayDate(date)} 待确认今日任务。`, - bubble_text: confirmed - ? '我已接入 qimingclaw 客户端执行链路,正在等待真实 Run/Event 数据接入。' - : '请先确认今天要执行的任务,我会把它们映射到 qimingclaw 执行链路。', + status_label: hasLiveRuntime + ? (failureCount > 0 ? '有异常' : runningCount > 0 ? '执行中' : '已同步') + : confirmed ? '运行中' : '待确认', + status_tone: hasLiveRuntime + ? (failureCount > 0 ? 'danger' : runningCount > 0 ? 'running' : 'success') + : confirmed ? 'running' : 'waiting', + summary: hasLiveRuntime + ? `当前日期 ${displayDate(date)} 已读取 ${liveRecordCount} 条 qimingclaw 本地运行记录。` + : confirmed + ? `当前日期 ${displayDate(date)} 已确认 ${selectedCount} 项任务。` + : `当前日期 ${displayDate(date)} 待确认今日任务。`, + bubble_text: hasLiveRuntime + ? '我已接入 qimingclaw 本地运行记录,会持续展示真实任务、事件和同步状态。' + : confirmed + ? '我已接入 qimingclaw 客户端执行链路,正在等待真实 Run/Event 数据接入。' + : '请先确认今天要执行的任务,我会把它们映射到 qimingclaw 执行链路。', }, overview: { title: '今日执行', - status_label: confirmed ? '真实数据' : '待确认', - status_tone: confirmed ? 'success' : 'waiting', + 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) }, ], @@ -713,13 +1584,17 @@ function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): Digita events, execution_summaries: executionSummaries, run_details: buildRunDetails(events), - decisions: [], + decisions: buildWorkdayDecisions(state, date, snapshot), delivery: { - headline: confirmed ? 'qimingclaw 执行链路已接入' : '等待确认任务设置', - summary: '当前阶段已将数字员工页面切换到 qimingclaw adapter,并开始读取客户端服务状态。', + headline: hasLiveRuntime ? 'qimingclaw 本地记录已接入' : confirmed ? 'qimingclaw 执行链路已接入' : '等待确认任务设置', + summary: hasLiveRuntime + ? '数字员工页面正在读取本地 Plan / Task / Run / Event,并同步展示管理端 outbox 状态。' + : '当前阶段已将数字员工页面切换到 qimingclaw adapter,并开始读取客户端服务状态。', lines: [ '已吸收 sgRobot 数字员工 UI 和核心展示模型。', - '已建立 qimingclaw workday/plans/tasks/skills API adapter。', + 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'), @@ -745,20 +1620,20 @@ function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): Digita totals: { task_count: selectedCount, execution_count: events.length, - success_count: 0, - failure_count: 0, + success_count: successCount, + failure_count: failureCount, running_count: runningCount, }, task_sections: executionSummaries, detail_lines: events.map((event) => `${event.time} ${event.task_title}:${event.detail}`), - pdf_available: false, + pdf_available: Boolean(generatedAt), pdf_url: null, }, }; } -function handleWorkdayAction(body: DigitalEmployeeWorkdayActionRequest, snapshot: QimingclawSnapshot | null): DigitalEmployeeWorkdayProjection { - const state = readState(); +async function handleWorkdayAction(body: DigitalEmployeeWorkdayActionRequest, snapshot: QimingclawSnapshot | null): Promise { + const state = snapshot?.uiState ?? await readBridgeUiState() ?? readState(); const date = body.date || todayInputDate(); if (body.action === 'confirm_workday') { @@ -781,12 +1656,163 @@ function handleWorkdayAction(body: DigitalEmployeeWorkdayActionRequest, snapshot 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', + }; + } - writeState(state); - return buildWorkday(date, snapshot); + const savedState = await saveState( + state, + body.action, + date, + body.action === 'decide_approval' + ? { decision_id: body.decision_id, decision: body.decision } + : undefined, + ); + return buildWorkday(date, { ...(snapshot ?? { generatedAt: new Date().toISOString(), services: {}, sessions: { total: 0, active: 0, items: [] } }), uiState: savedState }); +} + +async function handleGovernanceCommand( + body: GovernanceCommandRequest, + snapshot: QimingclawSnapshot | null, +): Promise { + 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 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()}`; + return 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, + }); + } + + if (body.command_kind === 'activate_plan' || body.command_kind === 'run_schedule_now') { + const targetPlanId = planId || runtimeDebugPlans(snapshot)[0]?.plan_id || PLANS[0]?.plan_id || 'qimingclaw-client-readiness'; + const dispatch = await dispatchPlan(targetPlanId, snapshot); + return governanceAccepted(commandId, correlationId, '已映射为 qimingclaw 本地任务激活。', { + plan_id: targetPlanId, + status: 'active', + dispatched_tasks: dispatch.dispatched_tasks, + source: 'qimingclaw-adapter', + }); + } + + if (body.command_kind === 'create_schedule') { + return governanceAccepted(commandId, correlationId, '已记录 qimingclaw 兼容定时任务请求。', { + schedule_id: scheduleId || `qimingclaw-schedule-${Date.now()}`, + plan_id: planId, + status: 'enabled', + source: 'qimingclaw-adapter', + }); + } + + if (['submit_plan', 'approve_plan', 'amend_plan', 'resume_plan', 'pause_plan', 'cancel_plan'].includes(body.command_kind)) { + return 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', + }); + } + + return { + command_id: commandId, + accepted: false, + correlation_id: correlationId, + error: 'missing_capability', + message: `qimingclaw 数字员工兼容层尚未支持 ${body.command_kind}。`, + }; +} + +function governanceAccepted( + commandId: string, + correlationId: string, + message: string, + result: Record, +): GovernanceCommandResponse { + return { + command_id: commandId, + accepted: true, + correlation_id: correlationId, + message, + result, + }; +} + +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 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 本地运行记录'; + 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', + }, + ...tasks.map((task, index) => ({ + seq: index + 2, + 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', + })), + ...runs.map((run, index) => ({ + seq: tasks.length + index + 2, + 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(PLANS.find((candidate) => candidate.plan_id === planId) ?? PLANS[0]!, snapshot); const timestamp = new Date().toISOString(); return { @@ -839,20 +1865,76 @@ function buildPlanFlow(planId: string, snapshot: QimingclawSnapshot | null = nul }; } -function dispatchPlan(planId: string): PlanDispatchResponse { - const state = readState(); +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): Promise { + const state = snapshot?.uiState ?? await readBridgeUiState() ?? readState(); const date = todayInputDate(); const selected = new Set(state.selectedDutyIdsByDate[date] ?? []); selected.add(planId); state.selectedDutyIdsByDate[date] = [...selected]; state.confirmedDates[date] = state.confirmedDates[date] ?? new Date().toISOString(); - writeState(state); + await saveState(state, 'confirm_workday', date); return { ok: true, plan_id: planId, source_plan_id: planId, - dispatched_tasks: buildTasks(planId).map((task) => ({ + dispatched_tasks: buildTasks(planId, snapshot).map((task) => ({ task_id: task.task_id, title: task.title, status: task.status === 'pending' ? 'running' : task.status, @@ -860,7 +1942,23 @@ function dispatchPlan(planId: string): PlanDispatchResponse { }; } -function buildPlanMessages(planId: string): Array<{ role: string; content: string; created_at?: string | null }> { +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 = PLANS.find((candidate) => candidate.plan_id === planId) ?? PLANS[0]!; return [ { diff --git a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/pages/digital/CommandDeck.tsx b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/pages/digital/CommandDeck.tsx index 2172ab03..3ee702f9 100644 --- a/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/pages/digital/CommandDeck.tsx +++ b/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/pages/digital/CommandDeck.tsx @@ -4,7 +4,6 @@ import { Clipboard, Clock3, Download, Eye, PlayCircle, RefreshCw } from 'lucide- import Avatar3D from '@/components/digital/Avatar3D'; import { basePath } from '@/lib/basePath'; import { - digitalEmployeeReportPdfUrl, dispatchPlanNow, getDigitalEmployeeWorkday, getSchedulerJobs, @@ -14,6 +13,7 @@ import { import { isTauri } from '@/lib/tauri'; import type { CronJob, + DigitalEmployeeDailyReport, DigitalEmployeeDecision, DigitalEmployeeDuty, DigitalEmployeeRunDetail, @@ -590,8 +590,8 @@ function syncFailureDiagnosticFileName(failure: ManagementSyncFailure): string { return `qimingclaw-sync-failure-${entity || failure.id || 'diagnostic'}.json`; } -function downloadTextFile(filename: string, content: string): void { - const blob = new Blob([content], { type: 'application/json;charset=utf-8' }); +function downloadTextFile(filename: string, content: string, mimeType = 'application/json;charset=utf-8'): void { + const blob = new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; @@ -602,6 +602,47 @@ function downloadTextFile(filename: string, content: string): void { window.setTimeout(() => URL.revokeObjectURL(url), 1000); } +function dailyReportFileName(date: string): string { + return `qimingclaw-digital-report-${date}.txt`; +} + +function buildDailyReportDownloadText( + dateLabel: string, + report: DigitalEmployeeDailyReport, + summaries: DigitalEmployeeTaskExecutionSummary[], + details: DigitalEmployeeRunDetail[], +): string { + return [ + report.title || `${dateLabel} 数字员工日报`, + `日期:${dateLabel}`, + `生成时间:${report.generated_at ? formatBeijingDateTime(report.generated_at) : '暂未生成'}`, + `生成来源:${report.generated_by === 'ai' ? 'AI' : 'qimingclaw adapter'}`, + '', + '## 摘要', + report.summary || '暂无摘要。', + '', + '## 指标', + `任务总数:${report.totals.task_count}`, + `执行次数:${report.totals.execution_count}`, + `成功次数:${report.totals.success_count}`, + `失败次数:${report.totals.failure_count}`, + `运行中:${report.totals.running_count ?? 0}`, + '', + '## 任务汇总', + ...(summaries.length > 0 + ? summaries.map((summary) => `- ${summary.title}:${summary.latest_status_label},执行 ${summary.total_count} 次,成功 ${summary.success_count},失败 ${summary.failure_count}。${summary.ai_summary || summary.business_summary || ''}`) + : ['暂无任务汇总。']), + '', + '## 运行明细', + ...(details.length > 0 + ? details.map((detail) => `- ${detail.time || '--:--'} ${detail.task_name} / ${detail.step_name || detail.operation_name || '任务执行'}:${detail.status_label},${detail.business_detail}`) + : ['暂无运行明细。']), + '', + '## 详情', + ...((report.detail_lines ?? []).length > 0 ? report.detail_lines : ['暂无详情。']), + ].join('\n'); +} + async function readManagementSyncStatus(): Promise { const bridge = window.QimingClawBridge?.digital; if (!bridge?.getSyncStatus) return null; @@ -734,8 +775,6 @@ function resolveEmployeeVisualState(args: { return 'skill'; } -const DECISION_CHAIN_TODO_TEXT = '真实审批链路待接入'; - function readGuideDismissedDate(): string | null { if (typeof window === 'undefined') return null; try { @@ -1207,7 +1246,7 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit : await getDigitalEmployeeWorkday(selectedDate); if (!mountedRef.current) return; setWorkdayProjection(latestProjection); - setActionNotice('日报已生成,可下载 PDF。'); + setActionNotice('日报已生成,可下载本地文本。'); if (latestProjection.daily_report?.status === 'ready') { setReportPreviewOpen(true); } @@ -1220,18 +1259,31 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit } }, [reportScheduleTime, selectedDate, selectedDutyIds, selectedSubmissionDutyIds]); - const downloadDailyReportPdf = useCallback(() => { - window.open(digitalEmployeeReportPdfUrl(selectedDate), '_blank', 'noopener,noreferrer'); - }, [selectedDate]); + const downloadDailyReport = useCallback(() => { + if (!dailyReport || dailyReport.status !== 'ready') return; + downloadTextFile( + dailyReportFileName(selectedDate), + buildDailyReportDownloadText(selectedDateLabel, dailyReport, taskSummaryCards, visibleRunDetails), + 'text/plain;charset=utf-8', + ); + setActionNotice('日报文本已下载。'); + }, [dailyReport, selectedDate, selectedDateLabel, taskSummaryCards, visibleRunDetails]); const resolveDecision = useCallback(async (decision: DigitalEmployeeDecision, actionId: string) => { setActionBusy(`${decision.id}:${actionId}`); setActionError(null); setActionNotice(null); try { + const projection = await postDigitalEmployeeWorkdayAction({ + action: 'decide_approval', + date: selectedDate, + decision_id: decision.id, + decision: actionId, + }); if (!mountedRef.current) return; const actionLabel = decision.actions.find((action) => action.id === actionId)?.label ?? actionId; - setActionNotice(`待实现:已触发“${actionLabel}”演示效果,${DECISION_CHAIN_TODO_TEXT},未改动后端审批或执行状态。`); + setWorkdayProjection(projection); + setActionNotice(`已记录“${actionLabel}”处理结果,并写入 qimingclaw 本地运行状态。`); } catch (error) { if (mountedRef.current) { setActionError(error instanceof Error ? error.message : '处理决策失败'); @@ -1239,7 +1291,7 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit } finally { if (mountedRef.current) setActionBusy(null); } - }, []); + }, [selectedDate]); const openDuty = useCallback((planItem: DigitalPlanItem) => { onNavigate({ tab: 'missions', date: selectedDate, missionId: planItem.conversationId || planItem.id }); @@ -1804,8 +1856,8 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit type="button" className="de-btn-secondary de-btn-sm de-report-download-btn" disabled={!dailyReport?.pdf_available} - onClick={downloadDailyReportPdf} - title={dailyReport?.pdf_available ? '下载日报 PDF' : '生成日报后可下载日报 PDF'} + onClick={downloadDailyReport} + title={dailyReport?.pdf_available ? '下载本地日报文本' : '生成日报后可下载本地日报'} >