Files
qiming/qimingclaw/crates/agent-electron-client/embedded/sgrobot-digital/src/lib/qimingclawAdapter.ts
2026-06-10 22:29:52 +08:00

6281 lines
257 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type {
ApprovalEntry,
ApprovalSubscriptionPreferences,
ApprovalTimelineResponse,
ArtifactEntry,
CanonicalEvent,
CronJob,
CronRun,
CostSummary,
DashboardResponse,
DebugPlan,
DebugRun,
DebugStoreResponse,
DebugTask,
DiagResult,
DigitalEmployeeDailyReport,
DigitalEmployeeDecision,
DigitalEmployeeDiagnosticSummary,
DigitalEmployeeDuty,
DigitalEmployeeManagedService,
DigitalEmployeePlanAgentState,
DigitalEmployeeRouteSummary,
DigitalEmployeeRunDetail,
DigitalEmployeeTaskAgentState,
DigitalEmployeeTaskExecutionSummary,
DigitalEmployeeWorkEvent,
DigitalEmployeeWorkdayActionRequest,
DigitalEmployeeWorkdayProjection,
FlowResponse,
GovernanceCommandRequest,
GovernanceCommandResponse,
IngressRouteRequest,
IngressRouteResponse,
MemoryEntry,
NotificationPreferences,
PlanDispatchResponse,
ReadyStepDispatchResult,
PlanStepDispatchPolicy,
PlanStepSummary,
RouteDecisionRecord,
RouteDecisionTimelineResponse,
SkillSpec,
UserNotification,
} 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>;
setSkillEnabled?: (skillId: string, enabled: boolean) => Promise<{ ok?: boolean; skill_id?: string; enabled?: boolean; error?: string }>;
getPlanTemplates?: () => Promise<QimingclawPlanTemplateList>;
listMemories?: (options?: { query?: string; category?: string; limit?: number }) => Promise<QimingclawMemoryRecord[]>;
addMemory?: (input: QimingclawMemoryUpsertInput) => Promise<QimingclawMemoryRecord | null>;
deleteMemory?: (keyOrId: string) => Promise<QimingclawMemoryRecord | null>;
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 }>;
dispatchPlanReadySteps?: (planId: string) => Promise<ReadyStepDispatchResult>;
getScheduleRuns?: (scheduleId: string, limit?: number) => Promise<QimingclawScheduleRunRecord[]>;
listRouteDecisions?: (options?: { limit?: number }) => Promise<RouteDecisionRecord[]>;
recordRouteDecision?: (input: QimingclawRouteDecisionInput) => Promise<RouteDecisionRecord>;
recordGovernanceCommand?: (command: QimingclawGovernanceCommandRecord) => Promise<unknown>;
recordDailyReport?: (input: QimingclawDailyReportRecordInput) => Promise<QimingclawDailyReportRecordResult | null>;
accessArtifact?: (artifactId: string, action: 'preview' | 'download' | 'open_location', options?: { actor?: string | null; source?: string | null }) => Promise<QimingclawArtifactAccessResponse>;
recordArtifactLifecycle?: (input: QimingclawArtifactLifecycleInput) => Promise<QimingclawArtifactLifecycleResult | null>;
recordApprovalAction?: (input: QimingclawApprovalActionInput) => Promise<QimingclawApprovalActionResult | null>;
restartManagedService?: (serviceId: string, options?: { reason?: string | null; actor?: string | null }) => Promise<QimingclawManagedServiceActionResponse>;
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>>;
notificationStateById?: Record<string, NotificationState>;
notificationPreferences?: NotificationPreferences;
approvalSubscriptionPreferences?: ApprovalSubscriptionPreferences;
planStepDispatchPolicy?: PlanStepDispatchPolicy;
consoleRole?: string;
rolePreferences?: Record<string, unknown>;
profile: {
name: string;
company: string;
position: string;
currentStatus: string;
};
}
interface NotificationState {
status?: UserNotification['status'];
read_at?: string | null;
dismissed_at?: string | null;
reply?: string | null;
replied_at?: string | null;
}
const NOTIFICATION_LEVELS: UserNotification['level'][] = ['info', 'warn', 'error', 'action'];
const NOTIFICATION_KINDS: UserNotification['kind'][] = [
'user_message',
'need_input',
'need_approval',
'task_progress',
'task_failed',
'task_finished',
'confirmation_pending',
'confirmation_resolved',
];
const APPROVAL_SUBSCRIPTION_ACTIONS: ApprovalSubscriptionPreferences['subscribed_actions'] = ['comment', 'delegate', 'mark_timeout', 'mark_risk', 'clear_risk', 'decision'];
const APPROVAL_SUBSCRIPTION_RISK_LEVELS: ApprovalSubscriptionPreferences['subscribed_risk_levels'] = ['normal', 'medium', 'high', 'critical'];
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;
taskId?: string | null;
runId?: string | null;
stepId?: string | null;
scheduleId?: string | null;
accepted?: boolean;
message?: string;
error?: string | null;
payload?: Record<string, unknown> | null;
result?: Record<string, unknown> | null;
}
interface QimingclawDailyReportRecordInput {
reportId?: string | null;
date: string;
title: string;
summary: string;
totals: Record<string, unknown>;
detailLines: string[];
artifactSources?: unknown[];
filename: string;
textContent: string;
generatedAt?: string | null;
reportScheduleTime?: string | null;
source?: string | null;
model?: string | null;
}
interface QimingclawDailyReportRecordResult {
reportId: string;
planId: string;
taskId: string;
runId: string;
artifactId: string;
eventId: string;
}
interface QimingclawArtifactAccessResponse {
ok: boolean;
artifact_id: string;
action: 'preview' | 'download' | 'open_location';
status: 'allowed' | 'rejected';
error?: string;
reason_codes?: string[];
preview?: Record<string, unknown>;
event_id?: string | null;
}
interface QimingclawArtifactLifecycleInput {
artifactId?: string | null;
action: string;
reason?: string | null;
retentionUntil?: string | null;
versionSummary?: Record<string, unknown> | null;
retentionSummary?: Record<string, unknown> | null;
deliverySummary?: Record<string, unknown> | null;
actor?: string | null;
source?: string | null;
planId?: string | null;
taskId?: string | null;
runId?: string | null;
}
interface QimingclawArtifactLifecycleResult {
eventId: string;
kind: string;
payload: Record<string, unknown>;
}
interface QimingclawApprovalActionInput {
approvalId?: string | null;
action: string;
comment?: string | null;
delegateTo?: string | null;
delegateLabel?: string | null;
dueAt?: string | null;
riskLevel?: string | null;
reason?: string | null;
actor?: string | null;
source?: string | null;
planId?: string | null;
taskId?: string | null;
runId?: string | null;
}
interface QimingclawApprovalActionResult {
eventId: string;
kind: string;
payload: Record<string, unknown>;
}
interface QimingclawManagedServiceActionResponse {
ok: boolean;
service_id: string;
action: 'restart';
status: 'success' | 'failed' | 'rejected';
message?: string;
error?: string;
managed_service?: DigitalEmployeeManagedService | null;
event_id?: string | null;
}
interface QimingclawRouteDecisionInput {
id?: string | null;
idempotencyKey?: string | null;
correlationId?: string | null;
routeKind: string;
intentKind: string;
reasonCodes?: string[];
candidateSkills?: string[];
contextRefs?: Record<string, unknown>;
createdCommandId?: string | null;
createsPlan?: boolean;
createsTaskRun?: boolean;
approvalMode?: string | null;
policyResult?: string | null;
entrypoint?: string | null;
actor?: string | null;
message?: string | null;
envelope?: Record<string, unknown> | null;
recordedAt?: string | null;
}
interface QimingclawRuntimeRecords {
generatedAt: string;
plans: QimingclawPlanRecord[];
planSteps?: QimingclawPlanStepRecord[];
schedules?: QimingclawScheduleRecord[];
scheduleRuns?: QimingclawScheduleRunRecord[];
tasks: QimingclawTaskRecord[];
runs: QimingclawRunRecord[];
events: QimingclawEventRecord[];
artifacts?: QimingclawArtifactRecord[];
approvals?: QimingclawApprovalRecord[];
memories?: QimingclawMemoryRecord[];
}
interface QimingclawMemoryRecord {
id: string;
remoteId?: string | null;
key: string;
content: string;
category: string;
source: string;
sourcePath?: string | null;
sessionId?: string | null;
score?: number | null;
status: string;
payload: unknown;
syncStatus: string;
lastSyncedAt?: string | null;
syncError?: string | null;
createdAt: string;
updatedAt: string;
deletedAt?: string | null;
}
interface QimingclawMemoryUpsertInput {
id?: string;
key?: string;
content: string;
category?: string;
source?: string;
sourcePath?: string | null;
sessionId?: string | null;
score?: number | null;
status?: string;
payload?: Record<string, unknown>;
}
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 执行',
},
};
const BUSINESS_VIEW_SOURCE = 'qimingclaw-runtime-projection';
type BusinessViewKind = 'plans' | 'plan-steps' | 'plan-step-graph' | 'tasks' | 'runs' | 'events' | 'artifacts' | 'approvals' | 'approval-history' | 'route-decisions' | 'notifications' | 'audits';
type BusinessViewRow = Record<string, unknown>;
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;
}
const managedServiceRestartMatch = pathname.match(/^\/api\/(?:digital-employee\/)?managed-services\/([^/]+)\/restart$/);
if (method === 'POST' && managedServiceRestartMatch) {
return await handleManagedServiceRestart(
decodeURIComponent(managedServiceRestartMatch[1] || ''),
parseOptionalJsonBody(options.body),
) as T;
}
const dailyReportDetailMatch = pathname.match(/^\/api\/digital-employee\/daily-reports\/([^/]+)$/);
if (method === 'GET' && dailyReportDetailMatch) {
const reportId = decodeURIComponent(dailyReportDetailMatch[1] || '');
const item = dailyReportRows(await readQimingclawSnapshot())
.find((row) => stringValue(row.report_id) === reportId || stringValue(row.artifact_id) === reportId) ?? null;
return { item, source: BUSINESS_VIEW_SOURCE } as T;
}
if (method === 'GET' && pathname === '/api/digital-employee/daily-reports') {
const rows = filterDailyReportRows(dailyReportRows(await readQimingclawSnapshot()), url.searchParams);
return { ...paginateBusinessRows(rows, url.searchParams), source: BUSINESS_VIEW_SOURCE } as T;
}
if (method === 'GET' && pathname === '/api/digital-employee/skill-call-audits') {
const rows = filterSkillCallAuditRows(skillCallAuditRows(await readQimingclawSnapshot()), url.searchParams);
return { ...paginateBusinessRows(rows, url.searchParams), source: BUSINESS_VIEW_SOURCE } as T;
}
if (method === 'GET' && pathname === '/api/digital-employee/artifact-access-events') {
const rows = filterArtifactAccessRows(artifactAccessRows(await readQimingclawSnapshot()), url.searchParams);
return { ...paginateBusinessRows(rows, url.searchParams), source: BUSINESS_VIEW_SOURCE } as T;
}
const artifactAccessMatch = pathname.match(/^\/api\/(?:digital-employee\/artifacts|artifact)\/([^/]+)\/access$/);
if (method === 'POST' && artifactAccessMatch) {
const artifactId = decodeURIComponent(artifactAccessMatch[1] || '');
return await handleArtifactAccess(artifactId, parseOptionalJsonBody(options.body)) as T;
}
const artifactRetentionMatch = pathname.match(/^\/api\/digital-employee\/artifacts\/([^/]+)\/retention$/);
if (method === 'POST' && artifactRetentionMatch) {
const artifactId = decodeURIComponent(artifactRetentionMatch[1] || '');
return await handleArtifactRetention(artifactId, parseOptionalJsonBody(options.body), await readQimingclawSnapshot()) as T;
}
const artifactVersionsMatch = pathname.match(/^\/api\/digital-employee\/artifacts\/([^/]+)\/versions$/);
if (method === 'GET' && artifactVersionsMatch) {
const artifactId = decodeURIComponent(artifactVersionsMatch[1] || '');
const snapshot = await readQimingclawSnapshot();
return { items: artifactVersionRows(snapshot, artifactId), source: BUSINESS_VIEW_SOURCE } as T;
}
const artifactDetailMatch = pathname.match(/^\/api\/digital-employee\/artifacts\/([^/]+)$/);
if (method === 'GET' && artifactDetailMatch) {
const artifactId = decodeURIComponent(artifactDetailMatch[1] || '');
const snapshot = await readQimingclawSnapshot();
const item = artifactDetailRow(snapshot, artifactId);
return { item, source: BUSINESS_VIEW_SOURCE } as T;
}
const businessPlanDetailMatch = pathname.match(/^\/api\/digital-employee\/plans\/([^/]+)$/);
if (method === 'GET' && businessPlanDetailMatch) {
const planId = decodeURIComponent(businessPlanDetailMatch[1] || '');
const item = businessPlanRows(await readQimingclawSnapshot())
.find((row) => stringValue(row.plan_id) === planId || stringValue(row.remote_id) === planId) ?? null;
return { item, source: BUSINESS_VIEW_SOURCE } as T;
}
const approvalActionMatch = pathname.match(/^\/api\/digital-employee\/approvals\/([^/]+)\/actions$/);
if (method === 'POST' && approvalActionMatch) {
const approvalId = decodeURIComponent(approvalActionMatch[1] || '');
return await handleApprovalAction(approvalId, parseOptionalJsonBody(options.body), await readQimingclawSnapshot()) as T;
}
if (method === 'GET' && pathname === '/api/approval/subscriptions') {
return approvalSubscriptionPreferences(notificationAdapterState(await readQimingclawSnapshot())) as T;
}
if (method === 'PATCH' && pathname === '/api/approval/subscriptions') {
return await handleApprovalSubscriptionPreferencesPatch(
parseJsonBody<Partial<ApprovalSubscriptionPreferences>>(options.body),
await readQimingclawSnapshot(),
) as T;
}
if (method === 'GET' && pathname === '/api/plan-step/dispatch-policy') {
return planStepDispatchPolicy(notificationAdapterState(await readQimingclawSnapshot())) as T;
}
if (method === 'PATCH' && pathname === '/api/plan-step/dispatch-policy') {
return await handlePlanStepDispatchPolicyPatch(
parseJsonBody<Partial<PlanStepDispatchPolicy>>(options.body),
await readQimingclawSnapshot(),
) as T;
}
if (method === 'GET' && pathname === '/api/digital-employee/plan-steps') {
const rows = filterPlanStepRows(planStepRows(await readQimingclawSnapshot()), url.searchParams);
return { ...paginateBusinessRows(rows, url.searchParams), source: BUSINESS_VIEW_SOURCE } as T;
}
if (method === 'GET' && pathname === '/api/digital-employee/plan-step-graph') {
const rows = filterPlanStepRows(planStepGraphRows(await readQimingclawSnapshot()), url.searchParams);
return { ...paginateBusinessRows(rows, url.searchParams), source: BUSINESS_VIEW_SOURCE } as T;
}
if (method === 'GET' && pathname === '/api/digital-employee/approval-history') {
const rows = filterApprovalHistoryRows(approvalHistoryRows(await readQimingclawSnapshot()), url.searchParams);
return { ...paginateBusinessRows(rows, url.searchParams), source: BUSINESS_VIEW_SOURCE } as T;
}
const businessViewMatch = pathname.match(/^\/api\/digital-employee\/(plans|plan-steps|plan-step-graph|tasks|runs|events|artifacts|approvals|approval-history|route-decisions|notifications|audits)$/);
if (method === 'GET' && businessViewMatch) {
return buildBusinessViewResponse(businessViewMatch[1] as BusinessViewKind, await readQimingclawSnapshot(), url.searchParams) as T;
}
if (method === 'GET' && pathname === '/api/route-decisions') {
return await readRouteDecisions(Number(url.searchParams.get('limit')) || 80) as T;
}
if (method === 'POST' && pathname === '/api/ingress/route') {
return await handleIngressRoute(parseJsonBody<IngressRouteRequest>(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' || method === 'POST') && pathname === '/api/doctor') {
return buildDoctor(await readQimingclawSnapshot()) as T;
}
if (method === 'GET' && pathname === '/api/cost') {
return { cost: buildCostSummary(await readQimingclawSnapshot()) } as T;
}
if (method === 'GET' && pathname === '/api/audit') {
return { events: buildAuditEvents(await readQimingclawSnapshot(), url.searchParams) } as T;
}
if (method === 'GET' && pathname === '/api/artifact') {
return { artifacts: buildArtifacts(await readQimingclawSnapshot()) } as T;
}
const approvalTimelineMatch = pathname.match(/^\/api\/approval\/([^/]+)\/timeline$/);
if (method === 'GET' && approvalTimelineMatch) {
return buildApprovalTimeline(decodeURIComponent(approvalTimelineMatch[1] || ''), await readQimingclawSnapshot()) as T;
}
if (method === 'GET' && pathname === '/api/notifications') {
return buildNotificationsResponse(url.searchParams.get('status'), await readQimingclawSnapshot()) as T;
}
if (method === 'GET' && pathname === '/api/notifications/preferences') {
return notificationPreferences(notificationAdapterState(await readQimingclawSnapshot())) as T;
}
if (method === 'PATCH' && pathname === '/api/notifications/preferences') {
return await handleNotificationPreferencesPatch(
parseJsonBody<Partial<NotificationPreferences>>(options.body),
await readQimingclawSnapshot(),
) as T;
}
if (method === 'GET' && pathname === '/api/notifications/unread-count') {
const snapshot = await readQimingclawSnapshot();
return { count: buildNotifications(snapshot, notificationAdapterState(snapshot)).filter((notification) => notification.status === 'unread').length } as T;
}
const notificationActionMatch = pathname.match(/^\/api\/notifications\/([^/]+)\/(read|dismiss|reply)$/);
if (method === 'POST' && notificationActionMatch) {
return await handleNotificationAction(
decodeURIComponent(notificationActionMatch[1] || ''),
notificationActionMatch[2] as 'read' | 'dismiss' | 'reply',
parseOptionalJsonBody(options.body),
await readQimingclawSnapshot(),
) as T;
}
if (method === 'GET' && pathname === '/api/memory') {
return { entries: await readMemoryEntries(url.searchParams.get('query'), url.searchParams.get('category')) } as T;
}
if (method === 'POST' && pathname === '/api/memory') {
return await handleMemoryCreate(parseJsonBody<Record<string, unknown>>(options.body)) as T;
}
const memoryDeleteMatch = pathname.match(/^\/api\/memory\/([^/]+)$/);
if (method === 'DELETE' && memoryDeleteMatch) {
await handleMemoryDelete(decodeURIComponent(memoryDeleteMatch[1] || ''));
return undefined 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;
}
const planReadyDispatchMatch = pathname.match(/^\/api\/digital-employee\/plans\/([^/]+)\/dispatch-ready-steps$/);
if (method === 'POST' && planReadyDispatchMatch) {
return await dispatchPlanReadySteps(decodeURIComponent(planReadyDispatchMatch[1] || ''), 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;
}
const debugPlanActionMatch = pathname.match(/^\/api\/debug\/plan\/([^/]+)\/([^/]+)$/);
if (method === 'POST' && debugPlanActionMatch) {
return await handlePlanAction(
decodeURIComponent(debugPlanActionMatch[1] || ''),
decodeURIComponent(debugPlanActionMatch[2] || ''),
await readQimingclawSnapshot(),
parseOptionalJsonBody(options.body),
) as T;
}
const planSubmitMatch = pathname.match(/^\/api\/plan\/([^/]+)\/submit$/);
if (method === 'POST' && planSubmitMatch) {
return await handlePlanAction(
decodeURIComponent(planSubmitMatch[1] || ''),
'submit',
await readQimingclawSnapshot(),
parseOptionalJsonBody(options.body),
) as T;
}
if (method === 'GET' && pathname.startsWith('/api/plan/') && pathname.endsWith('/revisions')) {
return buildPlanRevisions(decodeURIComponent(pathname.split('/')[3] || ''), await readQimingclawSnapshot()) as T;
}
const taskActionMatch = pathname.match(/^\/api\/task\/([^/]+)\/([^/]+)$/);
if (method === 'POST' && taskActionMatch) {
return await handleTaskAction(
decodeURIComponent(taskActionMatch[1] || ''),
decodeURIComponent(taskActionMatch[2] || ''),
await readQimingclawSnapshot(),
parseOptionalJsonBody(options.body),
) 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 await setBridgeSkillEnabled(skillId, body.enabled ?? true) as T;
}
return undefined;
}
function defaultState(): AdapterState {
return {
selectedDutyIdsByDate: {},
confirmedDates: {},
reportScheduleTime: DEFAULT_REPORT_TIME,
reportGeneratedAtByDate: {},
decisionResultsByDate: {},
notificationStateById: {},
notificationPreferences: defaultNotificationPreferences(),
approvalSubscriptionPreferences: defaultApprovalSubscriptionPreferences(),
planStepDispatchPolicy: defaultPlanStepDispatchPolicy(),
consoleRole: 'sales',
rolePreferences: {},
profile: {
name: '飞天数字员工',
company: 'qimingclaw 客户端',
position: '客户端任务执行员',
currentStatus: 'working',
},
};
}
function readState(): AdapterState {
try {
const raw = window.localStorage.getItem(STATE_KEY);
return normalizeAdapterState(raw ? JSON.parse(raw) as Partial<AdapterState> : {});
} catch {
return defaultState();
}
}
function normalizeAdapterState(value: Partial<AdapterState>): AdapterState {
return {
...defaultState(),
...value,
notificationPreferences: notificationPreferences(value),
approvalSubscriptionPreferences: approvalSubscriptionPreferences(value),
planStepDispatchPolicy: planStepDispatchPolicy(value),
};
}
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[]> {
const bridgeSkills = await readBridgeSkillCapabilities();
return dedupeSkills([...bridgeSkills, ...SKILLS]);
}
async function setBridgeSkillEnabled(
skillId: string,
enabled: boolean,
): Promise<{ ok: boolean; skill_id: string; enabled: boolean; error?: string }> {
const bridge = window.QimingClawBridge?.digital;
if (!bridge?.setSkillEnabled) {
return { ok: false, skill_id: skillId, enabled, error: 'qimingclaw_bridge_unavailable' };
}
try {
const result = await bridge.setSkillEnabled(skillId, enabled);
return {
ok: result?.ok !== false,
skill_id: result?.skill_id ?? skillId,
enabled: result?.enabled ?? enabled,
...(result?.error ? { error: result.error } : {}),
};
} catch (error) {
return {
ok: false,
skill_id: skillId,
enabled,
error: error instanceof Error ? error.message : String(error),
};
}
}
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,
taskId: stringFromUnknown(response.result?.task_id)
|| stringFromUnknown(body.context_refs?.task_id)
|| stringFromUnknown(body.payload?.task_id)
|| null,
runId: stringFromUnknown(response.result?.run_id)
|| stringFromUnknown(body.context_refs?.run_id)
|| stringFromUnknown(body.payload?.run_id)
|| stringFromUnknown(body.payload?.latest_run_id)
|| null,
stepId: stringFromUnknown(response.result?.step_id)
|| stringFromUnknown(body.context_refs?.step_id)
|| stringFromUnknown(body.payload?.step_id)
|| stringFromUnknown(body.payload?.stepId)
|| 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 };
}
}
async function readMemoryEntries(query?: string | null, category?: string | null): Promise<MemoryEntry[]> {
try {
const records = await window.QimingClawBridge?.digital?.listMemories?.({
query: query || undefined,
category: category || undefined,
limit: 300,
}) ?? [];
return records.map(memoryEntryFromRecord);
} catch {
return [];
}
}
async function handleMemoryCreate(body: Record<string, unknown>): Promise<{ ok: true; entry: MemoryEntry | null }> {
const key = stringFromUnknown(body.key);
const content = stringFromUnknown(body.content ?? body.text);
const category = stringFromUnknown(body.category) || 'fact';
if (!content) return { ok: true, entry: null };
const record = await window.QimingClawBridge?.digital?.addMemory?.({
key: key || undefined,
content,
category,
source: 'qimingclaw-digital-page',
payload: {
source: 'embedded-memory-api',
key: key || null,
},
}) ?? null;
return { ok: true, entry: record ? memoryEntryFromRecord(record) : null };
}
async function handleMemoryDelete(keyOrId: string): Promise<void> {
try {
await window.QimingClawBridge?.digital?.deleteMemory?.(keyOrId);
} catch {
// Keep the DELETE endpoint best-effort so the UI can refresh into the true state.
}
}
function memoryEntryFromRecord(record: QimingclawMemoryRecord): MemoryEntry {
return {
id: record.id,
key: record.key || record.id,
content: record.content,
category: record.category || 'fact',
timestamp: record.updatedAt || record.createdAt || new Date().toISOString(),
session_id: record.sessionId ?? null,
score: typeof record.score === 'number' ? record.score : null,
};
}
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 parseOptionalJsonBody(body: BodyInit | null | undefined): Record<string, unknown> {
return parseJsonBody<Record<string, unknown>>(body);
}
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} 项需处理`;
}
async function handleManagedServiceRestart(
serviceId: string,
body: Record<string, unknown>,
): Promise<QimingclawManagedServiceActionResponse> {
const bridge = window.QimingClawBridge?.digital;
if (!bridge?.restartManagedService) {
const snapshot = await readQimingclawSnapshot();
return {
ok: false,
service_id: serviceId,
action: 'restart',
status: 'failed',
error: 'bridge_unavailable',
managed_service: buildManagedServices(snapshot).find((service) => service.service_id === serviceId) ?? null,
};
}
try {
const result = await bridge.restartManagedService(serviceId, {
reason: stringValue(body.reason) || 'digital_employee_managed_service_restart',
actor: stringValue(body.actor) || 'digital_employee_ui',
});
const snapshot = await readQimingclawSnapshot();
return {
...result,
managed_service: result.managed_service ?? buildManagedServices(snapshot).find((service) => service.service_id === serviceId) ?? null,
};
} catch (error) {
const snapshot = await readQimingclawSnapshot();
return {
ok: false,
service_id: serviceId,
action: 'restart',
status: 'failed',
error: error instanceof Error ? error.message : String(error),
managed_service: buildManagedServices(snapshot).find((service) => service.service_id === serviceId) ?? null,
};
}
}
async function handleArtifactAccess(
artifactId: string,
body: Record<string, unknown>,
): Promise<QimingclawArtifactAccessResponse> {
const bridge = window.QimingClawBridge?.digital;
const action = normalizeArtifactAccessAction(stringValue(body.action));
if (!bridge?.accessArtifact) {
return {
ok: false,
artifact_id: artifactId,
action,
status: 'rejected',
error: 'bridge_unavailable',
reason_codes: ['bridge_unavailable'],
};
}
try {
return await bridge.accessArtifact(artifactId, action, {
actor: stringValue(body.actor) || 'digital_employee_ui',
source: stringValue(body.source) || 'sgrobot-digital-adapter',
});
} catch (error) {
return {
ok: false,
artifact_id: artifactId,
action,
status: 'rejected',
error: error instanceof Error ? error.message : String(error),
reason_codes: ['artifact_access_failed'],
};
}
}
function normalizeArtifactAccessAction(value: string): 'preview' | 'download' | 'open_location' {
if (value === 'download' || value === 'open_location') return value;
return 'preview';
}
async function handleArtifactRetention(
artifactId: string,
body: Record<string, unknown>,
snapshot: QimingclawSnapshot | null,
): Promise<Record<string, unknown>> {
const bridge = window.QimingClawBridge?.digital;
const action = stringValue(body.action) || 'mark_review';
const artifact = artifactDetailRow(snapshot, artifactId);
if (!bridge?.recordArtifactLifecycle) {
return { ok: false, artifact_id: artifactId, action, status: 'rejected', error: 'bridge_unavailable' };
}
const result = await bridge.recordArtifactLifecycle({
artifactId,
action,
reason: stringValue(body.reason) || null,
retentionUntil: stringValue(body.retention_until ?? body.retentionUntil) || null,
versionSummary: artifact ? artifactVersionSummary(artifact) : null,
retentionSummary: artifact ? artifactRetentionSummaryForAction(artifact, action, body) : null,
deliverySummary: artifact ? artifactDeliverySummary(artifact) : null,
actor: stringValue(body.actor) || 'digital_employee_ui',
source: stringValue(body.source) || 'sgrobot-digital-adapter',
planId: artifact ? stringValue(artifact.plan_id) || null : null,
taskId: artifact ? stringValue(artifact.task_id) || null : null,
runId: artifact ? stringValue(artifact.run_id) || null : null,
});
return {
ok: Boolean(result && result.kind !== 'artifact_retention_rejected'),
artifact_id: artifactId,
action,
status: result?.kind === 'artifact_retention_rejected' ? 'rejected' : 'recorded',
event_id: result?.eventId ?? null,
item: artifact,
};
}
async function handleApprovalAction(
approvalId: string,
body: Record<string, unknown>,
snapshot: QimingclawSnapshot | null,
): Promise<Record<string, unknown>> {
const bridge = window.QimingClawBridge?.digital;
const action = stringValue(body.action) || 'comment';
const approval = approvalDetailRow(snapshot, approvalId);
if (!bridge?.recordApprovalAction) {
return { ok: false, approval_id: approvalId, action, status: 'rejected', error: 'bridge_unavailable' };
}
const result = await bridge.recordApprovalAction({
approvalId,
action,
comment: stringValue(body.comment) || null,
delegateTo: stringValue(body.delegate_to ?? body.delegateTo) || null,
delegateLabel: stringValue(body.delegate_label ?? body.delegateLabel) || null,
dueAt: stringValue(body.due_at ?? body.dueAt) || null,
riskLevel: stringValue(body.risk_level ?? body.riskLevel) || null,
reason: stringValue(body.reason) || null,
actor: stringValue(body.actor) || 'digital_employee_ui',
source: stringValue(body.source) || 'sgrobot-digital-adapter',
planId: approval ? stringValue(approval.plan_id) || null : null,
taskId: approval ? stringValue(approval.task_id) || null : null,
runId: approval ? stringValue(approval.run_id) || null : null,
});
return {
ok: Boolean(result && result.kind !== 'approval_action_rejected'),
approval_id: approvalId,
action,
status: result?.kind === 'approval_action_rejected' ? 'rejected' : 'recorded',
event_id: result?.eventId ?? null,
item: approval,
};
}
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', 'cancelled'].includes(normalized)) return 'danger';
if (['running', 'active', 'leased', 'syncing'].includes(normalized)) return 'running';
return 'waiting';
}
function taskStatusLabel(status: string): string {
const normalized = status.toLowerCase();
if (normalized === 'ready') return '待启动';
if (normalized === 'blocked') return '依赖阻塞';
if (normalized === 'skipped') return '已跳过';
if (normalized === 'cancelled') return '已取消';
if (normalized === 'paused') return '已暂停';
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);
const normalizedStatus = task.status.toLowerCase();
const stepId = taskStepId(task);
return {
agent_id: `task-agent-${slugifyIdPart(task.id)}`,
task_id: task.id,
plan_id: task.planId ?? null,
step_id: stepId,
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: ['failed', 'cancelled', 'skipped'].includes(normalizedStatus),
can_skip: ['pending', 'running'].includes(normalizedStatus),
can_cancel: ['pending', 'running', 'paused'].includes(normalizedStatus),
next_action: taskAgentNextAction(normalizedStatus, tone),
updated_at: latestRun?.updatedAt ?? task.updatedAt,
};
});
}
function taskAgentNextAction(
status: string,
tone: ReturnType<typeof taskStatusTone>,
): string {
if (['failed', 'cancelled', 'skipped'].includes(status)) return 'retry_or_review';
if (status === 'paused') return 'resume_or_cancel';
if (tone === 'running') return 'watch_progress';
if (tone === 'success') return 'collect_artifacts';
return 'start_run';
}
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 buildDoctor(snapshot: QimingclawSnapshot | null): { results: DiagResult[]; summary: Record<string, unknown> } {
const now = new Date().toISOString();
const managedServices = buildManagedServices(snapshot);
const tasks = buildTasks(null, snapshot);
const approvals = buildApprovals(snapshot);
const runtimeErrors = runtimeEvents(snapshot).filter((event) => /fail|error|reject|cancel/i.test(`${event.kind} ${event.message}`));
const results: DiagResult[] = managedServices.map((service) => ({
id: `service:${service.service_id}`,
severity: service.requires_human_action || service.status === 'stopped' ? 'error' : service.status === 'degraded' ? 'warn' : 'ok',
category: 'service',
message: `${service.name}${service.status_label || service.status}`,
details: compactRecord({ error: service.error, kind: service.kind, running: service.running, dependent_tasks: service.dependent_tasks }),
source: 'qimingclaw-managed-service',
created_at: service.last_seen_at || now,
}));
const sync = snapshot?.sync;
results.push({
id: 'sync:management',
severity: doctorSeverityForSync(sync ?? null),
category: 'sync',
message: syncLine(snapshot),
details: sync ? compactRecord({ pending: sync.pending, failed: sync.failed, endpoint: sync.endpoint, missing_credentials: sync.missingCredentials, last_error: sync.lastSyncError }) : null,
source: 'qimingclaw-sync-status',
created_at: sync?.lastSyncAt ?? now,
});
for (const failure of sync?.recentFailures ?? []) {
results.push({
id: `sync_failure:${failure.id}`,
severity: failure.dueForRetry === false ? 'error' : 'warn',
category: 'sync',
message: syncFailureLine(failure),
details: compactRecord({ entity_type: failure.entityType, entity_id: failure.entityId, operation: failure.operation, attempts: failure.attempts, next_retry_at: failure.nextRetryAt }),
source: 'qimingclaw-sync-failure',
created_at: failure.lastAttemptAt ?? failure.updatedAt ?? failure.createdAt,
});
}
const failedTasks = tasks.filter((task) => taskStatusTone(task.status) === 'danger');
results.push({
id: 'runtime:tasks',
severity: failedTasks.length > 0 ? 'error' : tasks.length === 0 ? 'warn' : 'ok',
category: 'runtime',
message: tasks.length === 0 ? '尚未读取到 qimingclaw runtime task。' : `任务 ${tasks.length} 个,异常 ${failedTasks.length} 个。`,
details: compactRecord({ task_count: tasks.length, failed_task_count: failedTasks.length }),
source: 'qimingclaw-runtime-records',
created_at: now,
});
results.push({
id: 'runtime:approvals',
severity: approvals.some((approval) => approval.status === 'pending') ? 'warn' : 'ok',
category: 'approval',
message: `审批 ${approvals.length} 个,待处理 ${approvals.filter((approval) => approval.status === 'pending').length} 个。`,
details: compactRecord({ approval_count: approvals.length, pending_count: approvals.filter((approval) => approval.status === 'pending').length }),
source: 'qimingclaw-approval-records',
created_at: now,
});
if (runtimeErrors.length > 0) {
results.push({
id: 'runtime:events:error',
severity: 'error',
category: 'runtime',
message: `最近运行事件中发现 ${runtimeErrors.length} 条异常事实。`,
details: { event_ids: runtimeErrors.slice(0, 8).map((event) => event.id) },
source: 'qimingclaw-runtime-events',
created_at: runtimeErrors[0]?.occurredAt ?? now,
});
}
if (!hasRuntimeRecords(snapshot)) {
results.push({
id: 'runtime:data:empty',
severity: 'warn',
category: 'data',
message: '本地数字员工 runtime 记录为空,页面将只显示兼容投影。',
source: 'qimingclaw-runtime-records',
created_at: now,
});
}
return {
results,
summary: {
ok: results.filter((result) => result.severity === 'ok').length,
warn: results.filter((result) => result.severity === 'warn').length,
error: results.filter((result) => result.severity === 'error').length,
generated_at: now,
source: 'qimingclaw-runtime-projection',
},
};
}
function doctorSeverityForSync(sync: QimingclawSyncStatus | null): DiagResult['severity'] {
if (!sync) return 'warn';
if (sync.failed > 0 || sync.lastSyncError || (sync.missingCredentials?.length ?? 0) > 0) return 'error';
if (!sync.enabled || !sync.endpoint || sync.pending > 0) return 'warn';
return 'ok';
}
function buildCostSummary(snapshot: QimingclawSnapshot | null): CostSummary {
const requestCount = runtimeEvents(snapshot).length + runtimeRuns(snapshot).length + runtimeTasks(snapshot).length;
return {
session_cost_usd: 0,
daily_cost_usd: 0,
monthly_cost_usd: 0,
total_tokens: 0,
request_count: requestCount,
by_model: {
'qimingclaw-local': {
model: 'qimingclaw-local',
cost_usd: 0,
total_tokens: 0,
request_count: requestCount,
},
},
estimated: true,
source: 'qimingclaw-runtime-projection',
updated_at: snapshot?.generatedAt ?? new Date().toISOString(),
};
}
function buildAuditEvents(snapshot: QimingclawSnapshot | null, searchParams: URLSearchParams): Array<Record<string, unknown>> {
const limit = Math.max(1, Math.min(Math.floor(numberValue(searchParams.get('limit')) ?? 100), 300));
const category = stringValue(searchParams.get('category')) || stringValue(searchParams.get('kind'));
const level = stringValue(searchParams.get('level'));
const entityId = stringValue(searchParams.get('id'));
const events = [
...runtimeEvents(snapshot).map(auditEventFromRuntimeEvent),
...(snapshot?.sync?.recentFailures ?? []).map((failure) => ({
audit_id: `sync_failure:${failure.id}`,
category: 'sync',
action: 'sync_failed',
level: failure.dueForRetry === false ? 'error' : 'warn',
message: syncFailureLine(failure),
actor: 'qimingclaw-sync',
entity_type: failure.entityType,
entity_id: failure.entityId,
occurred_at: failure.lastAttemptAt ?? failure.updatedAt ?? failure.createdAt,
payload: failure,
})),
...buildArtifacts(snapshot).flatMap((artifact) => auditEventsFromArtifact(artifact)),
];
return events
.filter((event) => !category || stringValue(event.category) === category || stringValue(event.entity_type) === category)
.filter((event) => !level || stringValue(event.level) === level)
.filter((event) => !entityId || stringValue(event.entity_id) === entityId || stringValue(event.audit_id).includes(entityId))
.sort((left, right) => stringValue(right.occurred_at).localeCompare(stringValue(left.occurred_at)))
.slice(0, limit);
}
function buildDiagnosticSummary(snapshot: QimingclawSnapshot | null): DigitalEmployeeDiagnosticSummary {
const doctorResults = buildDoctor(snapshot).results;
const auditEvents = buildAuditEvents(snapshot, new URLSearchParams({ limit: '300' }));
const cost = buildCostSummary(snapshot);
const latestIssue = latestDiagnosticIssue(doctorResults, auditEvents);
return {
doctor: {
ok: doctorResults.filter((result) => result.severity === 'ok').length,
warn: doctorResults.filter((result) => result.severity === 'warn').length,
error: doctorResults.filter((result) => result.severity === 'error').length,
},
audit: {
info: auditEvents.filter((event) => stringValue(event.level) === 'info').length,
warn: auditEvents.filter((event) => stringValue(event.level) === 'warn').length,
error: auditEvents.filter((event) => stringValue(event.level) === 'error').length,
},
cost: {
request_count: cost.request_count,
estimated: Boolean(cost.estimated),
updated_at: cost.updated_at ?? null,
},
latest_issue: latestIssue,
};
}
function latestDiagnosticIssue(
doctorResults: DiagResult[],
auditEvents: Array<Record<string, unknown>>,
): DigitalEmployeeDiagnosticSummary['latest_issue'] {
const doctorError = latestDoctorIssue(doctorResults, 'error');
if (doctorError) return doctorError;
const auditError = latestAuditIssue(auditEvents, 'error');
if (auditError) return auditError;
const doctorWarn = latestDoctorIssue(doctorResults, 'warn');
if (doctorWarn) return doctorWarn;
return latestAuditIssue(auditEvents, 'warn');
}
function latestDoctorIssue(
doctorResults: DiagResult[],
level: 'warn' | 'error',
): DigitalEmployeeDiagnosticSummary['latest_issue'] {
const result = doctorResults
.filter((item) => item.severity === level)
.sort((left, right) => stringValue(right.created_at).localeCompare(stringValue(left.created_at)))[0];
if (!result) return null;
return {
id: stringValue(result.id) || `${result.category}:${level}`,
source: 'doctor',
level,
category: stringValue(result.category) || 'diagnostic',
message: result.message,
occurred_at: result.created_at ?? null,
};
}
function latestAuditIssue(
auditEvents: Array<Record<string, unknown>>,
level: 'warn' | 'error',
): DigitalEmployeeDiagnosticSummary['latest_issue'] {
const event = auditEvents.find((item) => stringValue(item.level) === level);
if (!event) return null;
return {
id: stringValue(event.audit_id) || `${stringValue(event.category) || 'audit'}:${level}`,
source: 'audit',
level,
category: stringValue(event.category) || stringValue(event.entity_type) || 'audit',
message: stringValue(event.message) || stringValue(event.action) || '审计事件需要关注',
occurred_at: stringValue(event.occurred_at) || null,
};
}
function auditRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
return buildAuditEvents(snapshot, new URLSearchParams({ limit: '300' })).map((event) => ({
...event,
audit_id: stringValue(event.audit_id),
status: stringValue(event.level) || 'info',
status_label: auditLevelLabel(stringValue(event.level)),
entity_id: stringValue(event.entity_id) || stringValue(event.audit_id),
updated_at: stringValue(event.occurred_at),
source: BUSINESS_VIEW_SOURCE,
}));
}
function filterAuditRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] {
const category = stringValue(searchParams.get('category')) || stringValue(searchParams.get('kind'));
const level = stringValue(searchParams.get('level'));
const id = stringValue(searchParams.get('id'));
return filterBusinessRows(rows, searchParams)
.filter((row) => !category || stringValue(row.category) === category || stringValue(row.entity_type) === category)
.filter((row) => !level || stringValue(row.level) === level)
.filter((row) => !id || stringValue(row.entity_id) === id || stringValue(row.audit_id).includes(id));
}
function auditLevelLabel(level: string): string {
if (level === 'error') return '异常';
if (level === 'warn') return '警告';
return '信息';
}
function buildBusinessViewResponse(kind: BusinessViewKind, snapshot: QimingclawSnapshot | null, searchParams: URLSearchParams): Record<string, unknown> {
const rowsByKind: Record<BusinessViewKind, BusinessViewRow[]> = {
plans: businessPlanRows(snapshot),
'plan-steps': planStepRows(snapshot),
'plan-step-graph': planStepGraphRows(snapshot),
tasks: businessTaskRows(snapshot),
runs: businessRunRows(snapshot),
events: businessEventRows(snapshot),
artifacts: businessArtifactRows(snapshot),
approvals: businessApprovalRows(snapshot),
'approval-history': approvalHistoryRows(snapshot),
'route-decisions': routeDecisionRows(snapshot),
notifications: notificationRows(snapshot, notificationAdapterState(snapshot)),
audits: auditRows(snapshot),
};
const rows = kind === 'route-decisions'
? filterRouteDecisionRows(rowsByKind[kind], searchParams)
: kind === 'plan-steps' || kind === 'plan-step-graph'
? filterPlanStepRows(rowsByKind[kind], searchParams)
: kind === 'approval-history'
? filterApprovalHistoryRows(rowsByKind[kind], searchParams)
: kind === 'notifications'
? filterNotificationRows(rowsByKind[kind], searchParams)
: kind === 'audits'
? filterAuditRows(rowsByKind[kind], searchParams)
: filterBusinessRows(rowsByKind[kind], searchParams);
return {
...paginateBusinessRows(rows, searchParams),
source: BUSINESS_VIEW_SOURCE,
};
}
function planStepRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
return runtimePlanSteps(snapshot)
.map((step) => {
const payload = asRecord(step.payload) ?? {};
const dependencyGraph = asRecord(payload.dependencyGraph ?? payload.dependency_graph) ?? {};
const lastDispatch = asRecord(payload.lastDispatch ?? payload.last_dispatch) ?? {};
const lease = activePlanStepDispatchLease(payload);
const readiness = planStepDispatchReadiness(step, snapshot, payload);
const blockedDependencies = uniqueStrings(arrayValue(dependencyGraph.blockedDependencies ?? dependencyGraph.blocked_dependencies));
const waitingDependencies = uniqueStrings(arrayValue(dependencyGraph.waitingDependencies ?? dependencyGraph.waiting_dependencies));
const satisfiedDependencies = uniqueStrings(arrayValue(dependencyGraph.satisfiedDependencies ?? dependencyGraph.satisfied_dependencies));
return {
step_id: step.id,
node_id: step.id,
remote_id: step.remoteId ?? null,
plan_id: step.planId,
title: step.title,
status: step.status,
seq: step.seq,
depends_on: step.dependsOn,
dependency_count: step.dependsOn.length,
dependency_status: stringValue(dependencyGraph.status) || stepDependencyStatus(step, blockedDependencies, waitingDependencies),
blocked_dependencies: blockedDependencies,
blocked_dependency_count: blockedDependencies.length,
blocked_dependency_details: planStepDependencyDetails(blockedDependencies, snapshot),
waiting_dependencies: waitingDependencies,
waiting_dependency_count: waitingDependencies.length,
satisfied_dependencies: satisfiedDependencies,
preferred_skill_ids: step.preferredSkillIds,
task_id: readiness.taskId,
latest_run_id: readiness.latestRunId,
dispatchable: readiness.dispatchable,
skip_reason: readiness.skipReason,
last_dispatch_status: stringValue(lastDispatch.status) || null,
last_dispatch_at: stringValue(lastDispatch.occurredAt ?? lastDispatch.occurred_at) || null,
lease,
lease_id: stringValue(lease?.lease_id ?? lease?.leaseId) || null,
lease_state: stringValue(lease?.state) || null,
lease_owner_device_id: stringValue(lease?.owner_device_id ?? lease?.ownerDeviceId) || null,
lease_acquired_at: stringValue(lease?.acquired_at ?? lease?.acquiredAt) || null,
lease_expires_at: stringValue(lease?.expires_at ?? lease?.expiresAt) || null,
lease_released_at: stringValue(lease?.released_at ?? lease?.releasedAt) || null,
lease_release_reason: stringValue(lease?.release_reason ?? lease?.releaseReason) || null,
lease_active: Boolean(lease),
sync_status: step.syncStatus,
last_synced_at: step.lastSyncedAt ?? null,
sync_error: step.syncError ?? null,
created_at: step.createdAt,
updated_at: step.updatedAt,
entity_type: 'plan_step',
entity_id: step.id,
source: BUSINESS_VIEW_SOURCE,
};
})
.sort((left, right) => stringValue(left.plan_id).localeCompare(stringValue(right.plan_id)) || Number(left.seq ?? 0) - Number(right.seq ?? 0) || stringValue(left.step_id).localeCompare(stringValue(right.step_id)));
}
function planStepGraphRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
return planStepRows(snapshot).map((row) => ({
node_id: stringValue(row.node_id) || stringValue(row.step_id),
step_id: stringValue(row.step_id),
plan_id: stringValue(row.plan_id) || null,
title: stringValue(row.title),
status: stringValue(row.status),
seq: row.seq,
depends_on: arrayValue(row.depends_on),
dependency_status: stringValue(row.dependency_status) || null,
blocked_dependencies: arrayValue(row.blocked_dependencies),
blocked_dependency_details: arrayValue(row.blocked_dependency_details),
waiting_dependencies: arrayValue(row.waiting_dependencies),
satisfied_dependencies: arrayValue(row.satisfied_dependencies),
task_id: stringValue(row.task_id) || null,
latest_run_id: stringValue(row.latest_run_id) || null,
dispatchable: row.dispatchable === true,
skip_reason: stringValue(row.skip_reason) || null,
lease: asRecord(row.lease) ?? null,
lease_id: stringValue(row.lease_id) || null,
lease_state: stringValue(row.lease_state) || null,
lease_owner_device_id: stringValue(row.lease_owner_device_id) || null,
lease_acquired_at: stringValue(row.lease_acquired_at) || null,
lease_expires_at: stringValue(row.lease_expires_at) || null,
lease_released_at: stringValue(row.lease_released_at) || null,
lease_release_reason: stringValue(row.lease_release_reason) || null,
lease_active: row.lease_active === true,
sync_status: stringValue(row.sync_status) || null,
updated_at: stringValue(row.updated_at) || null,
entity_type: 'plan_step_graph_node',
entity_id: stringValue(row.step_id),
source: BUSINESS_VIEW_SOURCE,
}));
}
function filterPlanStepRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] {
const planId = stringValue(searchParams.get('plan_id'));
const stepId = stringValue(searchParams.get('step_id')) || stringValue(searchParams.get('id'));
const status = stringValue(searchParams.get('status'));
const syncStatus = stringValue(searchParams.get('sync_status'));
return filterBusinessRows(rows, searchParams)
.filter((row) => !planId || stringValue(row.plan_id) === planId)
.filter((row) => !stepId || stringValue(row.step_id) === stepId || stringValue(row.node_id) === stepId || stringValue(row.entity_id) === stepId)
.filter((row) => !status || stringValue(row.status) === status)
.filter((row) => !syncStatus || stringValue(row.sync_status) === syncStatus);
}
function stepDependencyStatus(step: QimingclawPlanStepRecord, blockedDependencies: string[], waitingDependencies: string[]): string {
if (blockedDependencies.length > 0) return 'blocked';
if (waitingDependencies.length > 0) return 'waiting';
if (step.dependsOn.length > 0) return 'ready';
return 'independent';
}
function planStepDispatchReadiness(
step: QimingclawPlanStepRecord,
snapshot: QimingclawSnapshot | null,
payload: Record<string, unknown>,
): { taskId: string | null; latestRunId: string | null; dispatchable: boolean; skipReason: string | null } {
const tasks = runtimePlanTasks(snapshot, step.planId);
const task = tasks.find((candidate) => taskStepId(candidate) === step.id)
?? tasks.find((candidate) => !isTerminalRuntimeTaskStatus(candidate.status))
?? null;
const taskId = task?.id ?? null;
const latestRunId = task ? latestRunForTask(runtimeRuns(snapshot), task.id)?.id ?? null : null;
const lease = activePlanStepDispatchLease(payload);
let skipReason: string | null = null;
if (lease) skipReason = 'lease_active';
else if (step.status.toLowerCase() !== 'ready') skipReason = 'not_ready';
else if (!task) skipReason = 'missing_task';
else if (isTerminalRuntimeTaskStatus(task.status)) skipReason = 'task_terminal';
else if (runtimeRuns(snapshot).some((run) => run.taskId === task.id && isActiveRuntimeRunStatus(run.status))) skipReason = 'task_already_running';
else if (runtimeApprovals(snapshot).some((approval) => isOpenRuntimeApprovalStatus(approval.status) && (approval.planId === step.planId || approval.taskId === task.id || stringValue(asRecord(approval.payload)?.stepId ?? asRecord(approval.payload)?.step_id) === step.id))) skipReason = 'approval_pending';
else if (payload.auto_dispatch === false || payload.autoDispatch === false) skipReason = 'auto_dispatch_disabled';
return { taskId, latestRunId, dispatchable: !skipReason, skipReason };
}
function planStepDependencyDetails(dependencyIds: string[], snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
const stepsById = new Map(runtimePlanSteps(snapshot).map((step) => [step.id, step]));
return dependencyIds.map((dependencyId) => {
const dependency = stepsById.get(dependencyId);
if (!dependency) {
return {
step_id: dependencyId,
task_id: null,
latest_run_id: null,
title: dependencyId,
status: 'missing',
dependency_status: 'missing',
dispatchable: false,
skip_reason: 'missing_step',
};
}
const payload = asRecord(dependency.payload) ?? {};
const graph = asRecord(payload.dependencyGraph ?? payload.dependency_graph) ?? {};
const lease = activePlanStepDispatchLease(payload);
const readiness = planStepDispatchReadiness(dependency, snapshot, payload);
return {
step_id: dependency.id,
task_id: readiness.taskId,
latest_run_id: readiness.latestRunId,
title: dependency.title,
status: dependency.status,
dependency_status: stringValue(graph.status) || dependency.status,
dispatchable: readiness.dispatchable,
skip_reason: readiness.skipReason,
lease_state: stringValue(lease?.state) || null,
lease_active: Boolean(lease),
};
});
}
function activePlanStepDispatchLease(payload: Record<string, unknown>): Record<string, unknown> | null {
const lease = asRecord(payload.dispatchLease ?? payload.dispatch_lease);
if (!lease) return null;
const state = stringValue(lease.state) || 'active';
const expiresAt = stringValue(lease.expires_at ?? lease.expiresAt);
const leaseId = stringValue(lease.lease_id ?? lease.leaseId);
const ownerDeviceId = stringValue(lease.owner_device_id ?? lease.ownerDeviceId);
if (state !== 'active' || !expiresAt || expiresAt <= new Date().toISOString() || !leaseId || !ownerDeviceId) return null;
return lease;
}
function isTerminalRuntimeTaskStatus(status: string): boolean {
return ['completed', 'succeeded', 'success', 'done', 'skipped', 'cancelled', 'canceled'].includes(status.toLowerCase());
}
function isActiveRuntimeRunStatus(status: string): boolean {
return ['running', 'pending', 'queued', 'started'].includes(status.toLowerCase());
}
function isOpenRuntimeApprovalStatus(status: string): boolean {
return ['pending', 'reviewing', 'open', 'waiting'].includes(status.toLowerCase() || 'pending');
}
function routeDecisionRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
return runtimeEvents(snapshot)
.filter((event) => event.kind === 'route_decision')
.map((event) => {
const payload = asRecord(event.payload) ?? {};
const decision = routeDecisionFromRuntimePayload(payload, event);
const attentionLevel = routeDecisionAttentionLevel(decision);
return {
...decision,
route_decision_id: decision.id,
event_id: event.id,
status: decision.policy_result || 'accepted',
status_label: routeDecisionStatusLabel(decision),
attention_level: attentionLevel,
entity_type: 'route_decision',
entity_id: decision.id,
occurred_at: event.occurredAt,
created_at: event.createdAt,
updated_at: decision.recorded_at || event.occurredAt,
payload,
};
})
.sort((left, right) => stringValue(right.recorded_at).localeCompare(stringValue(left.recorded_at)) || stringValue(right.event_id).localeCompare(stringValue(left.event_id)));
}
function filterRouteDecisionRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] {
const routeKind = stringValue(searchParams.get('route_kind'));
const intentKind = stringValue(searchParams.get('intent_kind'));
const policyResult = stringValue(searchParams.get('policy_result'));
return filterBusinessRows(rows, searchParams)
.filter((row) => !routeKind || stringValue(row.route_kind) === routeKind)
.filter((row) => !intentKind || stringValue(row.intent_kind) === intentKind)
.filter((row) => !policyResult || stringValue(row.policy_result) === policyResult);
}
function notificationRows(snapshot: QimingclawSnapshot | null, state: AdapterState): BusinessViewRow[] {
const preferences = notificationPreferences(state);
return buildNotificationCandidates(snapshot, state)
.map((notification) => ({
notification_id: notification.notification_id,
kind: notification.kind,
level: notification.level,
status: notification.status,
title: notification.title,
body: notification.body,
plan_id: notification.plan_id,
task_id: notification.task_id,
correlation_id: notification.correlation_id,
created_at: notification.created_at,
updated_at: notification.replied_at || notification.dismissed_at || notification.read_at || notification.created_at,
read_at: notification.read_at ?? null,
dismissed_at: notification.dismissed_at ?? null,
replied_at: notification.replied_at ?? null,
muted_by_preferences: !notificationMatchesPreferences(notification, preferences),
entity_type: 'notification',
entity_id: notification.notification_id,
source: BUSINESS_VIEW_SOURCE,
}))
.sort((left, right) => stringValue(right.created_at).localeCompare(stringValue(left.created_at)) || stringValue(right.notification_id).localeCompare(stringValue(left.notification_id)));
}
function filterNotificationRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] {
const id = stringValue(searchParams.get('id'));
const kind = stringValue(searchParams.get('kind'));
const level = stringValue(searchParams.get('level'));
const muted = stringValue(searchParams.get('muted')).toLowerCase();
return filterBusinessRows(rows, searchParams)
.filter((row) => !id || stringValue(row.notification_id) === id || stringValue(row.correlation_id) === id || stringValue(row.entity_id) === id)
.filter((row) => !kind || stringValue(row.kind) === kind)
.filter((row) => !level || stringValue(row.level) === level)
.filter((row) => {
if (!muted) return true;
const mutedValue = row.muted_by_preferences === true;
if (['1', 'true', 'yes'].includes(muted)) return mutedValue;
if (['0', 'false', 'no'].includes(muted)) return !mutedValue;
return true;
});
}
function routeDecisionFromRuntimePayload(payload: Record<string, unknown>, event: QimingclawEventRecord): RouteDecisionRecord {
const decision = asRecord(payload.routeDecision ?? payload.route_decision ?? payload) ?? {};
return {
id: stringValue(decision.id) || event.id,
route_kind: stringValue(decision.route_kind ?? decision.routeKind) || 'ChatOnly',
intent_kind: stringValue(decision.intent_kind ?? decision.intentKind) || 'chat',
reason_codes: uniqueStrings(arrayValue(decision.reason_codes ?? decision.reasonCodes)),
candidate_skills: uniqueStrings(arrayValue(decision.candidate_skills ?? decision.candidateSkills)),
context_refs: asRecord(decision.context_refs ?? decision.contextRefs) ?? {},
created_command_id: stringValue(decision.created_command_id ?? decision.createdCommandId) || null,
correlation_id: stringValue(decision.correlation_id ?? decision.correlationId) || event.id,
creates_plan: Boolean(decision.creates_plan ?? decision.createsPlan),
creates_task_run: Boolean(decision.creates_task_run ?? decision.createsTaskRun),
approval_mode: stringValue(decision.approval_mode ?? decision.approvalMode) || null,
policy_result: stringValue(decision.policy_result ?? decision.policyResult) || 'accepted',
recorded_at: stringValue(decision.recorded_at ?? decision.recordedAt) || event.occurredAt,
entrypoint: stringValue(decision.entrypoint) || null,
actor: stringValue(decision.actor) || null,
};
}
function routeDecisionAttentionLevel(decision: RouteDecisionRecord): 'info' | 'action' | 'warn' | 'error' {
if (decision.policy_result && decision.policy_result !== 'accepted') return 'error';
if (decision.reason_codes.includes('candidate_skills_disabled') || decision.reason_codes.includes('route_action_missing_context')) return 'warn';
if (decision.created_command_id) return 'action';
return 'info';
}
function routeDecisionStatusLabel(decision: RouteDecisionRecord): string {
const attentionLevel = routeDecisionAttentionLevel(decision);
if (attentionLevel === 'error') return '策略拒绝';
if (attentionLevel === 'warn') return '需要关注';
if (decision.created_command_id) return '已映射命令';
return '已记录';
}
function buildRouteDecisionSummary(snapshot: QimingclawSnapshot | null): DigitalEmployeeRouteSummary {
const rows = routeDecisionRows(snapshot);
const latest = rows[0] ?? null;
return {
total: rows.length,
mapped_count: rows.filter((row) => stringValue(row.created_command_id)).length,
attention_count: rows.filter((row) => ['warn', 'error'].includes(stringValue(row.attention_level))).length,
latest: latest
? {
id: stringValue(latest.route_decision_id),
route_kind: stringValue(latest.route_kind),
intent_kind: stringValue(latest.intent_kind),
status_label: stringValue(latest.status_label),
attention_level: stringValue(latest.attention_level),
created_command_id: stringValue(latest.created_command_id) || null,
recorded_at: stringValue(latest.recorded_at),
}
: null,
};
}
function businessPlanRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
const runtimeById = new Map(runtimePlans(snapshot).map((plan) => [plan.id, plan]));
const tasks = buildTasks(null, snapshot);
const runs = buildTasksResponse(null, snapshot).runs;
const events = buildCanonicalEvents(snapshot, tasks, runs);
return filterPlans(null, snapshot).map((plan) => {
const runtime = runtimeById.get(plan.plan_id);
const relatedTasks = tasks.filter((task) => task.plan_id === plan.plan_id);
const relatedRuns = runs.filter((run) => run.plan_id === plan.plan_id);
const latestEventAt = events
.filter((event) => event.plan_id === plan.plan_id)
.map((event) => stringValue(event.occurred_at ?? event.timestamp))
.filter(Boolean)
.sort()
.pop() || null;
return {
plan_id: plan.plan_id,
remote_id: runtime?.remoteId ?? null,
title: plan.title,
objective: plan.objective ?? null,
status: plan.status,
task_count: relatedTasks.length,
run_count: relatedRuns.length,
latest_event_at: latestEventAt,
sync_status: (runtime?.syncStatus ?? stringValue(plan.sync_status)) || null,
created_at: plan.created_at,
updated_at: stringValue(plan.updated_at) || runtime?.updatedAt || plan.created_at,
payload: runtime?.payload ?? compactRecord({ source: plan.source, steps: plan.steps }),
};
});
}
function businessTaskRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
const runtimeById = new Map(runtimeTasks(snapshot).map((task) => [task.id, task]));
return buildTasks(null, snapshot).map((task) => {
const runtime = runtimeById.get(task.task_id);
return {
task_id: task.task_id,
remote_id: runtime?.remoteId ?? null,
plan_id: task.plan_id ?? null,
title: task.title,
status: task.status,
assigned_skill_id: task.assigned_skill_id || runtime?.assignedSkillId || null,
sync_status: (runtime?.syncStatus ?? stringValue(task.sync_status)) || null,
created_at: task.created_at,
updated_at: task.updated_at,
payload: runtime?.payload ?? compactRecord({ step_id: task.step_id, description: task.description }),
};
});
}
function businessRunRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
const runtimeById = new Map(runtimeRuns(snapshot).map((run) => [run.id, run]));
return buildTasksResponse(null, snapshot).runs.map((run) => {
const runtime = runtimeById.get(run.run_id);
const startedAt = runtime?.startedAt ?? run.created_at;
const finishedAt = runtime?.finishedAt ?? run.completed_at ?? null;
const latestRunEvent = (run.events ?? [])
.slice()
.sort((left, right) => stringValue(right.occurred_at).localeCompare(stringValue(left.occurred_at)))[0];
return {
run_id: run.run_id,
remote_id: runtime?.remoteId ?? null,
plan_id: run.plan_id ?? null,
task_id: run.task_id || null,
status: runtime?.status ?? run.lifecycle,
started_at: startedAt ?? null,
finished_at: finishedAt,
duration_ms: durationMs(startedAt, finishedAt),
last_event_message: latestRunEvent?.message ?? null,
sync_status: (runtime?.syncStatus ?? stringValue(run.sync_status)) || null,
created_at: runtime?.createdAt ?? run.created_at,
payload: runtime?.payload ?? compactRecord({ lifecycle: run.lifecycle, attempt_no: run.attempt_no }),
};
});
}
function businessEventRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
const tasks = buildTasks(null, snapshot);
const runs = buildTasksResponse(null, snapshot).runs;
const runtimeById = new Map(runtimeEvents(snapshot).map((event) => [event.id, event]));
return buildCanonicalEvents(snapshot, tasks, runs).map((event) => {
const runtime = runtimeById.get(event.event_id);
const planId = stringValue(event.plan_id) || runtime?.planId || null;
const taskId = stringValue(event.task_id) || runtime?.taskId || null;
return {
event_id: event.event_id,
remote_id: runtime?.remoteId ?? null,
plan_id: planId,
task_id: taskId,
run_id: runtime?.runId ?? null,
kind: event.kind ?? event.event_type ?? runtime?.kind ?? null,
message: event.message ?? runtime?.message ?? null,
occurred_at: stringValue(event.occurred_at ?? event.timestamp) || runtime?.occurredAt,
sync_status: runtime?.syncStatus ?? null,
payload: runtime?.payload ?? compactRecord({ aggregate_type: event.aggregate_type, aggregate_id: event.aggregate_id }),
};
});
}
function businessArtifactRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
const runtimeById = new Map(runtimeArtifacts(snapshot).map((artifact) => [artifact.id, artifact]));
const artifacts = buildArtifacts(snapshot);
return artifacts.map((artifact) => {
const artifactId = stringValue(artifact.artifact_id) || stringValue(artifact.subject_id);
const runtime = runtimeById.get(artifactId);
const entity = businessEntityFromRefs({
plan_id: artifact.plan_id,
task_id: artifact.task_id,
run_id: artifact.run_id,
fallback_type: 'artifact',
fallback_id: artifact.subject_id ?? artifactId,
});
const lifecycle = artifactLifecycleSummary(snapshot, artifactId);
const version = artifactVersionMeta(artifacts, artifact);
const delivery = artifactDeliverySummary(artifact);
return {
...artifact,
artifact_id: artifactId,
remote_id: (runtime?.remoteId ?? stringValue(artifact.remote_id)) || null,
entity_type: entity.entity_type,
entity_id: entity.entity_id,
sync_status: (runtime?.syncStatus ?? stringValue(artifact.sync_status)) || null,
version: version.version,
version_key: version.version_key,
version_rank: version.version_rank,
retention_status: lifecycle.retention_status,
retention_until: lifecycle.retention_until,
last_retention_action: lifecycle.last_retention_action,
last_access_status: stringValue(asRecord(artifact.access)?.last_access_status) || null,
delivery_summary: delivery,
};
});
}
function artifactDetailRow(snapshot: QimingclawSnapshot | null, artifactId: string): BusinessViewRow | null {
const row = businessArtifactRows(snapshot)
.find((artifact) => stringValue(artifact.artifact_id) === artifactId || stringValue(artifact.remote_id) === artifactId) ?? null;
if (!row) return null;
return {
...row,
versions: artifactVersionRows(snapshot, stringValue(row.artifact_id)),
lifecycle_events: artifactLifecycleRows(snapshot).filter((event) => stringValue(event.artifact_id) === stringValue(row.artifact_id)).slice(0, 20),
};
}
function artifactVersionRows(snapshot: QimingclawSnapshot | null, artifactId: string): BusinessViewRow[] {
const artifacts = buildArtifacts(snapshot);
const target = artifacts.find((artifact) => stringValue(artifact.artifact_id) === artifactId || stringValue(artifact.subject_id) === artifactId);
if (!target) return [];
const groupKey = artifactVersionGroupKey(target);
return artifacts
.filter((artifact) => artifactVersionGroupKey(artifact) === groupKey)
.sort((left, right) => stringValue(left.created_at).localeCompare(stringValue(right.created_at)))
.map((artifact, index) => {
const meta = artifactVersionMeta(artifacts, artifact);
return {
artifact_id: stringValue(artifact.artifact_id) || stringValue(artifact.subject_id),
name: stringValue(artifact.name) || null,
version: meta.version || `local-v${index + 1}`,
version_key: meta.version_key,
version_rank: index + 1,
delivery_summary: artifactDeliverySummary(artifact),
retention_status: artifactLifecycleSummary(snapshot, stringValue(artifact.artifact_id)).retention_status,
created_at: stringValue(artifact.created_at) || null,
source_label: stringValue(artifact.source_label) || null,
};
});
}
function businessApprovalRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
const runtimeById = new Map(runtimeApprovals(snapshot).map((approval) => [approval.id, approval]));
return buildApprovals(snapshot).map((approval) => {
const approvalId = stringValue(approval.approval_id);
const runtime = runtimeById.get(approvalId);
const summary = approvalActionSummary(snapshot, approvalId, approval);
const entity = businessEntityFromRefs({
plan_id: approval.plan_id,
task_id: approval.task_id,
run_id: approval.run_id,
fallback_type: stringValue(approval.source) === 'qimingclaw-sync' ? 'sync_outbox' : 'approval',
fallback_id: approval.task_id || approvalId,
});
return {
...approval,
approval_id: approvalId,
remote_id: (runtime?.remoteId ?? stringValue(approval.remote_id)) || null,
entity_type: entity.entity_type,
entity_id: entity.entity_id,
sync_status: (runtime?.syncStatus ?? stringValue(approval.sync_status)) || null,
risk_level: summary.risk_level,
sla_status: summary.sla_status,
due_at: summary.due_at,
last_action: summary.last_action,
last_actor: summary.last_actor,
comment_count: summary.comment_count,
delegate_to: summary.delegate_to,
};
});
}
function approvalDetailRow(snapshot: QimingclawSnapshot | null, approvalId: string): BusinessViewRow | null {
const row = businessApprovalRows(snapshot)
.find((approval) => stringValue(approval.approval_id) === approvalId || stringValue(approval.remote_id) === approvalId) ?? null;
if (!row) return null;
return {
...row,
timeline: buildApprovalTimeline(stringValue(row.approval_id), snapshot).events,
};
}
function approvalActionRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
return runtimeEvents(snapshot)
.filter((event) => ['approval_action_recorded', 'approval_action_rejected'].includes(event.kind))
.map((event) => {
const payload = asRecord(event.payload) ?? {};
const delegate = asRecord(payload.delegate_summary);
const comment = asRecord(payload.comment_summary);
const sla = asRecord(payload.sla_summary);
const risk = asRecord(payload.risk_summary);
return {
action_id: event.id,
event_id: event.id,
kind: event.kind,
approval_id: stringValue(payload.approval_id) || null,
action: stringValue(payload.action) || null,
status: stringValue(payload.status) || null,
comment_preview: stringValue(comment?.preview) || null,
comment_length: numberValue(comment?.length),
delegate_to: stringValue(delegate?.label ?? delegate?.target) || null,
sla_status: stringValue(sla?.sla_status) || null,
due_at: stringValue(sla?.due_at) || null,
risk_level: stringValue(risk?.risk_level) || null,
actor: stringValue(payload.actor) || null,
source: stringValue(payload.source) || null,
plan_id: (event.planId ?? stringValue(payload.plan_id)) || null,
task_id: (event.taskId ?? stringValue(payload.task_id)) || null,
run_id: (event.runId ?? stringValue(payload.run_id)) || null,
message: event.message,
occurred_at: event.occurredAt,
sync_status: event.syncStatus ?? null,
payload,
};
})
.sort((left, right) => stringValue(right.occurred_at).localeCompare(stringValue(left.occurred_at)));
}
function approvalHistoryRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
const approvalRows = new Map(businessApprovalRows(snapshot).map((approval) => [stringValue(approval.approval_id), approval]));
const actionRows = approvalActionRows(snapshot).map((action) => {
const approvalId = stringValue(action.approval_id);
const approval = approvalRows.get(approvalId);
return {
history_id: stringValue(action.action_id),
event_id: stringValue(action.event_id),
approval_id: approvalId || null,
action: stringValue(action.action) || null,
status: stringValue(action.status) || null,
actor: stringValue(action.actor) || null,
risk_level: stringValue(action.risk_level) || null,
due_at: stringValue(action.due_at) || null,
delegate_to: stringValue(action.delegate_to) || null,
comment_length: numberValue(action.comment_length),
message: stringValue(action.message) || null,
plan_id: stringValue(action.plan_id) || stringValue(approval?.plan_id) || null,
task_id: stringValue(action.task_id) || stringValue(approval?.task_id) || null,
run_id: stringValue(action.run_id) || stringValue(approval?.run_id) || null,
occurred_at: stringValue(action.occurred_at) || null,
updated_at: stringValue(action.occurred_at) || null,
sync_status: stringValue(action.sync_status) || null,
entity_type: 'approval_history',
entity_id: stringValue(action.action_id),
source: BUSINESS_VIEW_SOURCE,
};
});
const decisionRows = runtimeEvents(snapshot)
.filter((event) => event.kind === 'approval_decision')
.map((event) => {
const payload = asRecord(event.payload) ?? {};
const approvalId = stringValue(payload.approvalId ?? payload.approval_id) || event.id.split(':decision:')[0] || '';
const approval = approvalRows.get(approvalId);
return {
history_id: event.id,
event_id: event.id,
approval_id: approvalId || null,
action: 'decision',
status: stringValue(payload.nextStatus ?? payload.next_status ?? payload.decision) || null,
actor: stringValue(payload.actor) || stringValue(payload.source) || 'qimingclaw-approval-decision',
risk_level: stringValue(approval?.risk_level) || null,
due_at: stringValue(approval?.due_at) || null,
delegate_to: stringValue(approval?.delegate_to) || null,
comment_length: null,
message: event.message || null,
plan_id: event.planId || stringValue(payload.planId ?? payload.plan_id) || stringValue(approval?.plan_id) || null,
task_id: event.taskId || stringValue(payload.taskId ?? payload.task_id) || stringValue(approval?.task_id) || null,
run_id: event.runId || stringValue(payload.runId ?? payload.run_id) || stringValue(approval?.run_id) || null,
occurred_at: event.occurredAt,
updated_at: event.occurredAt,
sync_status: event.syncStatus ?? null,
entity_type: 'approval_history',
entity_id: event.id,
source: BUSINESS_VIEW_SOURCE,
};
});
return [...actionRows, ...decisionRows]
.sort((left, right) => stringValue(right.occurred_at).localeCompare(stringValue(left.occurred_at)) || stringValue(right.history_id).localeCompare(stringValue(left.history_id)));
}
function filterApprovalHistoryRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] {
const approvalId = stringValue(searchParams.get('approval_id')) || stringValue(searchParams.get('id'));
const action = stringValue(searchParams.get('action'));
const riskLevel = stringValue(searchParams.get('risk_level'));
return filterBusinessRows(rows, searchParams)
.filter((row) => !approvalId || stringValue(row.approval_id) === approvalId || stringValue(row.history_id) === approvalId || stringValue(row.event_id) === approvalId)
.filter((row) => !action || stringValue(row.action) === action)
.filter((row) => !riskLevel || stringValue(row.risk_level) === riskLevel);
}
function approvalActionSummary(
snapshot: QimingclawSnapshot | null,
approvalId: string,
approval: Record<string, unknown>,
): Record<string, unknown> {
const actions = approvalActionRows(snapshot).filter((event) => stringValue(event.approval_id) === approvalId);
const latest = actions[0];
const latestRisk = actions.find((event) => stringValue(event.risk_level));
const latestSla = actions.find((event) => {
const status = stringValue(event.sla_status);
return Boolean(status && status !== 'untracked');
});
const latestDelegate = actions.find((event) => stringValue(event.delegate_to));
const payload = asRecord(approval.payload);
const dueAt = stringValue(latestSla?.due_at) || stringValue(payload?.due_at ?? payload?.dueAt ?? payload?.timeout_at ?? payload?.timeoutAt);
const slaStatus = stringValue(latestSla?.sla_status) || approvalSlaStatus(dueAt);
return {
risk_level: stringValue(latestRisk?.risk_level) || 'normal',
sla_status: slaStatus,
due_at: dueAt || null,
last_action: stringValue(latest?.action) || null,
last_actor: stringValue(latest?.actor) || null,
comment_count: actions.filter((event) => stringValue(event.action) === 'comment').length,
delegate_to: stringValue(latestDelegate?.delegate_to) || null,
};
}
function approvalSlaStatus(dueAt: string): string {
if (!dueAt) return 'untracked';
const dueMs = Date.parse(dueAt);
if (!Number.isFinite(dueMs)) return 'untracked';
return dueMs < Date.now() ? 'timeout' : 'tracked';
}
function skillCallAuditRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
return runtimeEvents(snapshot)
.filter((event) => ['skill_call_allowed', 'skill_call_rejected', 'skill_call_observed'].includes(event.kind))
.map((event) => {
const payload = asRecord(event.payload) ?? {};
return {
audit_id: event.id,
event_id: event.id,
remote_id: event.remoteId ?? null,
decision: stringValue(payload.decision) || event.kind.replace('skill_call_', ''),
source: stringValue(payload.source) || null,
server_id: stringValue(payload.server_id) || null,
tool_name: stringValue(payload.tool_name) || null,
skill_id: stringValue(payload.skill_id) || null,
call_id: stringValue(payload.call_id) || null,
session_id: stringValue(payload.session_id) || null,
task_id: (event.taskId ?? stringValue(payload.task_id)) || null,
run_id: (event.runId ?? stringValue(payload.run_id)) || null,
reason_codes: Array.isArray(payload.reason_codes) ? payload.reason_codes : [],
message: event.message,
occurred_at: event.occurredAt,
sync_status: event.syncStatus ?? null,
payload,
};
})
.sort((left, right) => stringValue(right.occurred_at).localeCompare(stringValue(left.occurred_at)));
}
function filterSkillCallAuditRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] {
const skillId = stringValue(searchParams.get('skill_id'));
const serverId = stringValue(searchParams.get('server_id'));
const toolName = stringValue(searchParams.get('tool_name'));
const decision = stringValue(searchParams.get('decision'));
return rows
.filter((row) => !skillId || stringValue(row.skill_id) === skillId)
.filter((row) => !serverId || stringValue(row.server_id) === serverId)
.filter((row) => !toolName || stringValue(row.tool_name) === toolName)
.filter((row) => !decision || stringValue(row.decision) === decision);
}
function artifactAccessRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
return runtimeEvents(snapshot)
.filter((event) => ['artifact_access_allowed', 'artifact_access_rejected'].includes(event.kind))
.map((event) => {
const payload = asRecord(event.payload) ?? {};
return {
access_id: event.id,
event_id: event.id,
remote_id: event.remoteId ?? null,
artifact_id: stringValue(payload.artifact_id) || null,
action: stringValue(payload.action) || null,
status: stringValue(payload.status) || event.kind.replace('artifact_access_', ''),
reason_codes: Array.isArray(payload.reason_codes) ? payload.reason_codes : [],
actor: stringValue(payload.actor) || null,
source: stringValue(payload.source) || null,
plan_id: (event.planId ?? stringValue(payload.plan_id)) || null,
task_id: (event.taskId ?? stringValue(payload.task_id)) || null,
run_id: (event.runId ?? stringValue(payload.run_id)) || null,
message: event.message,
occurred_at: event.occurredAt,
sync_status: event.syncStatus ?? null,
payload,
};
})
.sort((left, right) => stringValue(right.occurred_at).localeCompare(stringValue(left.occurred_at)));
}
function filterArtifactAccessRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] {
const artifactId = stringValue(searchParams.get('artifact_id'));
const action = stringValue(searchParams.get('action'));
const status = stringValue(searchParams.get('status'));
return rows
.filter((row) => !artifactId || stringValue(row.artifact_id) === artifactId)
.filter((row) => !action || stringValue(row.action) === action)
.filter((row) => !status || stringValue(row.status) === status);
}
function artifactLifecycleRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
return runtimeEvents(snapshot)
.filter((event) => ['artifact_retention_marked', 'artifact_retention_rejected', 'artifact_version_observed'].includes(event.kind))
.map((event) => {
const payload = asRecord(event.payload) ?? {};
return {
lifecycle_id: event.id,
event_id: event.id,
kind: event.kind,
artifact_id: stringValue(payload.artifact_id) || null,
action: stringValue(payload.action) || null,
status: stringValue(payload.status) || null,
retention_status: stringValue(payload.retention_status) || null,
retention_until: stringValue(payload.retention_until) || null,
reason: stringValue(payload.reason) || null,
actor: stringValue(payload.actor) || null,
source: stringValue(payload.source) || null,
plan_id: (event.planId ?? stringValue(payload.plan_id)) || null,
task_id: (event.taskId ?? stringValue(payload.task_id)) || null,
run_id: (event.runId ?? stringValue(payload.run_id)) || null,
message: event.message,
occurred_at: event.occurredAt,
sync_status: event.syncStatus ?? null,
payload,
};
})
.sort((left, right) => stringValue(right.occurred_at).localeCompare(stringValue(left.occurred_at)));
}
function artifactLifecycleSummary(snapshot: QimingclawSnapshot | null, artifactId: string): Record<string, unknown> {
const latest = artifactLifecycleRows(snapshot)
.find((event) => stringValue(event.artifact_id) === artifactId && stringValue(event.kind) === 'artifact_retention_marked');
return {
retention_status: stringValue(latest?.retention_status) || 'unmanaged',
retention_until: stringValue(latest?.retention_until) || null,
last_retention_action: stringValue(latest?.action) || null,
last_retention_at: stringValue(latest?.occurred_at) || null,
};
}
function artifactRetentionSummaryForAction(
artifact: BusinessViewRow,
action: string,
body: Record<string, unknown>,
): Record<string, unknown> {
return {
previous_status: stringValue(artifact.retention_status) || 'unmanaged',
retention_status: retentionStatusForAction(action),
retention_until: stringValue(body.retention_until ?? body.retentionUntil) || null,
reason: stringValue(body.reason) || null,
};
}
function retentionStatusForAction(action: string): string | null {
if (action === 'mark_keep') return 'keep';
if (action === 'mark_expire') return 'expire_candidate';
if (action === 'mark_review') return 'review_required';
if (action === 'clear_retention') return 'unmanaged';
return null;
}
function artifactVersionSummary(artifact: BusinessViewRow): Record<string, unknown> {
return {
version: stringValue(artifact.version) || null,
version_key: stringValue(artifact.version_key) || null,
version_rank: numberValue(artifact.version_rank),
file_id: stringValue(artifact.file_id) || null,
workspace_id: stringValue(artifact.workspace_id) || null,
project_id: stringValue(artifact.project_id) || null,
created_at: stringValue(artifact.created_at) || null,
name: stringValue(artifact.name) || null,
};
}
function artifactDeliverySummary(artifact: Record<string, unknown>): Record<string, unknown> {
return {
delivery_status: stringValue(artifact.delivery_status) || null,
delivery_stage: stringValue(artifact.delivery_stage) || null,
workspace_id: stringValue(artifact.workspace_id) || null,
project_id: stringValue(artifact.project_id) || null,
file_id: stringValue(artifact.file_id) || null,
version: stringValue(artifact.version) || null,
file_size: numberValue(artifact.file_size),
source_tool: stringValue(artifact.source_tool) || null,
};
}
function artifactVersionMeta(artifacts: ArtifactEntry[], artifact: Record<string, unknown>): Record<string, unknown> {
const groupKey = artifactVersionGroupKey(artifact);
const group = artifacts
.filter((item) => artifactVersionGroupKey(item) === groupKey)
.sort((left, right) => stringValue(left.created_at).localeCompare(stringValue(right.created_at)));
const artifactId = stringValue(artifact.artifact_id) || stringValue(artifact.subject_id);
const index = Math.max(0, group.findIndex((item) => stringValue(item.artifact_id) === artifactId || stringValue(item.subject_id) === artifactId));
return {
version: stringValue(artifact.version) || `local-v${index + 1}`,
version_key: `${groupKey}:v${stringValue(artifact.version) || index + 1}`,
version_rank: index + 1,
};
}
function artifactVersionGroupKey(artifact: Record<string, unknown>): string {
const fileId = stringValue(artifact.file_id);
const workspaceId = stringValue(artifact.workspace_id);
const name = stringValue(artifact.name);
if (fileId) return `file:${workspaceId}:${fileId}:${name}`;
const artifactId = stringValue(artifact.artifact_id) || stringValue(artifact.subject_id);
if (artifactId) return `artifact:${artifactId}`;
return `fallback:${workspaceId}:${stringValue(artifact.project_id)}:${name || stringValue(artifact.uri)}`;
}
function dailyReportRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
return buildArtifacts(snapshot)
.filter(isDailyReportArtifact)
.map((artifact) => {
const payload = asRecord(artifact.payload) ?? {};
const artifactId = stringValue(artifact.artifact_id) || stringValue(payload.artifact_id) || stringValue(artifact.subject_id);
const reportId = stringValue(payload.report_id) || stringValue(artifact.report_id) || artifactId.replace(/:artifact:text$/, '');
const generatedAt = stringValue(payload.generated_at) || stringValue(artifact.created_at) || new Date().toISOString();
const date = stringValue(payload.date) || generatedAt.slice(0, 10);
return {
report_id: reportId,
artifact_id: artifactId,
event_id: stringValue(payload.event_id) || `${reportId}:event:generated`,
date,
title: stringValue(payload.title) || stringValue(artifact.name) || `${date} 数字员工日报`,
status: stringValue(payload.status) || 'ready',
summary: stringValue(payload.summary) || stringValue(artifact.value) || '',
totals: asRecord(payload.totals) ?? {},
detail_lines: arrayValue(payload.detail_lines),
artifact_sources: arrayValue(payload.artifact_sources),
filename: stringValue(payload.filename) || stringValue(artifact.name) || dailyReportFilename(date),
format: stringValue(payload.format) || 'text',
text_content: stringValue(payload.text_content) || stringValue(artifact.value),
generated_at: generatedAt,
sync_status: stringValue(artifact.sync_status) || null,
entity_type: 'artifact',
entity_id: artifactId,
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) || generatedAt,
updated_at: generatedAt,
payload,
};
})
.sort((left, right) => stringValue(right.generated_at).localeCompare(stringValue(left.generated_at)));
}
function isDailyReportArtifact(artifact: ArtifactEntry): boolean {
const payload = asRecord(artifact.payload) ?? {};
return stringValue(artifact.kind) === 'daily_report'
|| stringValue(payload.kind) === 'daily_report'
|| Boolean(stringValue(payload.report_id) && stringValue(payload.text_content));
}
function filterDailyReportRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] {
const date = stringValue(searchParams.get('date'));
return filterBusinessRows(rows, searchParams)
.filter((row) => !date || stringValue(row.date) === date);
}
function filterBusinessRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] {
const entityId = stringValue(searchParams.get('entity_id'));
const remoteId = stringValue(searchParams.get('remote_id'));
const status = stringValue(searchParams.get('status'));
const syncStatus = stringValue(searchParams.get('sync_status'));
const updatedFrom = stringValue(searchParams.get('updated_from'));
const updatedTo = stringValue(searchParams.get('updated_to'));
return rows
.filter((row) => !entityId || businessRowEntityIds(row).includes(entityId))
.filter((row) => !remoteId || stringValue(row.remote_id) === remoteId)
.filter((row) => !status || stringValue(row.status) === status)
.filter((row) => !syncStatus || stringValue(row.sync_status) === syncStatus)
.filter((row) => {
const timestamp = businessRowUpdatedAt(row);
if (updatedFrom && (!timestamp || timestamp < updatedFrom)) return false;
if (updatedTo && (!timestamp || timestamp > updatedTo)) return false;
return true;
});
}
function paginateBusinessRows(rows: BusinessViewRow[], searchParams: URLSearchParams): { items: BusinessViewRow[]; page_no: number; page_size: number; total: number } {
const pageNo = Math.max(1, Math.floor(numberValue(searchParams.get('page_no')) ?? 1));
const pageSize = Math.max(1, Math.min(Math.floor(numberValue(searchParams.get('page_size')) ?? 20), 100));
const start = (pageNo - 1) * pageSize;
return {
items: rows.slice(start, start + pageSize),
page_no: pageNo,
page_size: pageSize,
total: rows.length,
};
}
function businessEntityFromRefs(refs: Record<string, unknown>): { entity_type: string; entity_id: string | null } {
const runId = stringValue(refs.run_id);
if (runId) return { entity_type: 'run', entity_id: runId };
const taskId = stringValue(refs.task_id);
if (taskId) return { entity_type: 'task', entity_id: taskId };
const planId = stringValue(refs.plan_id);
if (planId) return { entity_type: 'plan', entity_id: planId };
return {
entity_type: stringValue(refs.fallback_type) || 'record',
entity_id: stringValue(refs.fallback_id) || null,
};
}
function businessRowEntityIds(row: BusinessViewRow): string[] {
return uniqueStrings([
row.entity_id,
row.plan_id,
row.task_id,
row.run_id,
row.event_id,
row.history_id,
row.step_id,
row.node_id,
row.artifact_id,
row.approval_id,
row.notification_id,
row.report_id,
]);
}
function businessRowUpdatedAt(row: BusinessViewRow): string {
return stringValue(row.updated_at)
|| stringValue(row.finished_at)
|| stringValue(row.started_at)
|| stringValue(row.occurred_at)
|| stringValue(row.latest_event_at)
|| stringValue(row.created_at);
}
function durationMs(startedAt?: string | null, finishedAt?: string | null): number | null {
if (!startedAt || !finishedAt) return null;
const started = Date.parse(startedAt);
const finished = Date.parse(finishedAt);
if (!Number.isFinite(started) || !Number.isFinite(finished) || finished < started) return null;
return finished - started;
}
function auditEventFromRuntimeEvent(event: QimingclawEventRecord): Record<string, unknown> {
const payload = asRecord(event.payload) ?? {};
const category = event.kind.startsWith('governance_')
? 'governance'
: event.kind === 'route_decision'
? 'route'
: event.kind === 'approval_decision'
? 'approval'
: 'runtime';
const entityType = event.runId ? 'run' : event.taskId ? 'task' : event.planId ? 'plan' : 'event';
return {
audit_id: event.id,
category,
action: event.kind,
level: /fail|error|reject|cancel/i.test(`${event.kind} ${event.message}`) ? 'error' : 'info',
message: event.message || event.kind,
actor: stringValue(payload.actor) || stringValue(payload.source) || 'qimingclaw-runtime',
entity_type: entityType,
entity_id: event.runId ?? event.taskId ?? event.planId ?? event.id,
occurred_at: event.occurredAt,
payload,
};
}
function auditEventsFromArtifact(artifact: ArtifactEntry): Array<Record<string, unknown>> {
const deliveryStatus = stringValue(artifact.delivery_status);
if (!deliveryStatus) return [];
return [{
audit_id: `artifact_delivery:${artifact.artifact_id ?? artifact.subject_id}`,
category: 'artifact',
action: stringValue(artifact.delivery_stage) || 'artifact_delivery',
level: deliveryStatus === 'failed' || deliveryStatus === 'error' ? 'error' : 'info',
message: `${artifact.name || artifact.kind || 'Artifact'}${deliveryStatus}`,
actor: stringValue(artifact.source_tool) || 'qimingclaw-file-server',
entity_type: 'artifact',
entity_id: artifact.artifact_id ?? artifact.subject_id,
occurred_at: stringValue(artifact.created_at) || new Date().toISOString(),
payload: artifact,
}];
}
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 buildApprovalTimeline(approvalId: string, snapshot: QimingclawSnapshot | null): ApprovalTimelineResponse {
const approval = buildApprovals(snapshot).find((candidate) => stringValue(candidate.approval_id) === approvalId) ?? null;
const runtimeApproval = runtimeApprovals(snapshot).find((candidate) => candidate.id === approvalId) ?? null;
const approvalPayload = asRecord(approval?.payload ?? runtimeApproval?.payload);
const approvalAcpPermission = acpPermissionFromPayload(approvalPayload);
const refs = {
approvalId,
planId: stringValue(approval?.plan_id) || runtimeApproval?.planId || '',
taskId: stringValue(approval?.task_id) || runtimeApproval?.taskId || '',
runId: stringValue(approval?.run_id) || runtimeApproval?.runId || '',
permissionId: approvalAcpPermission.permissionId,
sessionId: approvalAcpPermission.sessionId,
};
const approvalCreatedAt = stringValue(approval?.created_at) || runtimeApproval?.createdAt || runtimeApproval?.updatedAt || '';
const entries: ApprovalTimelineResponse['events'] = [];
if (approval || runtimeApproval) {
const title = stringValue(approval?.title) || runtimeApproval?.title || '待确认事项';
const status = stringValue(approval?.status) || runtimeApproval?.status || 'pending';
entries.push({
event_id: `${approvalId}:approval_record`,
kind: 'approval_record',
status,
message: `审批记录:${title} -> ${status}`,
occurred_at: approvalCreatedAt || new Date().toISOString(),
payload: approvalPayload,
source_type: 'approval',
});
}
for (const event of runtimeEvents(snapshot)) {
if (!isApprovalTimelineEvent(event, refs)) continue;
const payload = asRecord(event.payload);
entries.push({
event_id: event.id,
kind: event.kind,
status: approvalTimelineEventStatus(event, payload),
message: event.message || event.kind,
occurred_at: event.occurredAt,
payload,
source_type: stringValue(payload?.source) || 'runtime_event',
});
}
return {
approval_id: approvalId,
approval,
events: dedupeApprovalTimelineEvents(entries)
.sort((left, right) => left.occurred_at.localeCompare(right.occurred_at) || left.event_id.localeCompare(right.event_id)),
};
}
function acpPermissionFromPayload(payload: Record<string, unknown> | null): { permissionId: string; sessionId: string } {
const nested = asRecord(payload?.acpPermission) ?? asRecord(payload?.acp_permission) ?? asRecord(payload?.permission);
return {
permissionId: stringValue(nested?.permission_id) || stringValue(nested?.permissionId) || stringValue(payload?.permission_id) || stringValue(payload?.permissionId),
sessionId: stringValue(nested?.session_id) || stringValue(nested?.sessionId) || stringValue(payload?.session_id) || stringValue(payload?.sessionId),
};
}
function isApprovalTimelineEvent(
event: QimingclawEventRecord,
refs: { approvalId: string; planId: string; taskId: string; runId: string; permissionId: string; sessionId: string },
): boolean {
const payload = asRecord(event.payload);
const payloadApprovalId = stringValue(payload?.approvalId)
|| stringValue(payload?.approval_id)
|| stringValue(payload?.decision_id)
|| stringValue(payload?.decisionId);
if (payloadApprovalId === refs.approvalId) return true;
if (event.id.startsWith(`${refs.approvalId}:`)) return true;
const acpPermission = acpPermissionFromPayload(payload);
if (refs.permissionId && acpPermission.permissionId === refs.permissionId) return true;
if (refs.sessionId && acpPermission.sessionId === refs.sessionId) return true;
const approvalLikeKind = event.kind.includes('approval') || event.kind.includes('permission');
if (!approvalLikeKind) return false;
if (refs.runId && event.runId === refs.runId) return true;
if (refs.taskId && event.taskId === refs.taskId) return true;
if (refs.planId && event.planId === refs.planId) return true;
return false;
}
function approvalTimelineEventStatus(event: QimingclawEventRecord, payload: Record<string, unknown> | null): string | null {
return stringValue(payload?.nextStatus)
|| stringValue(payload?.next_status)
|| stringValue(payload?.status)
|| stringValue(payload?.decision)
|| (event.kind === 'approval_decision' ? 'completed' : null);
}
function dedupeApprovalTimelineEvents(events: ApprovalTimelineResponse['events']): ApprovalTimelineResponse['events'] {
const seen = new Set<string>();
return events.filter((event) => {
if (seen.has(event.event_id)) return false;
seen.add(event.event_id);
return true;
});
}
function notificationAdapterState(snapshot: QimingclawSnapshot | null): AdapterState {
return snapshot?.uiState ? normalizeAdapterState(snapshot.uiState) : readState();
}
function buildNotificationsResponse(status: string | null, snapshot: QimingclawSnapshot | null): { notifications: UserNotification[] } {
const notifications = buildNotifications(snapshot, notificationAdapterState(snapshot));
const normalizedStatus = (status || 'all').toLowerCase();
if (normalizedStatus === 'all') {
return { notifications: notifications.filter((notification) => notification.status !== 'dismissed') };
}
return { notifications: notifications.filter((notification) => notification.status === normalizedStatus) };
}
function buildNotifications(snapshot: QimingclawSnapshot | null, state: AdapterState): UserNotification[] {
return buildNotificationCandidates(snapshot, state)
.filter((notification) => notificationMatchesPreferences(notification, state.notificationPreferences));
}
function buildNotificationCandidates(snapshot: QimingclawSnapshot | null, state: AdapterState): UserNotification[] {
const notifications = dedupeNotifications([
...approvalNotifications(snapshot),
...approvalActionNotifications(snapshot),
...syncFailureNotifications(snapshot),
...managedServiceNotifications(snapshot),
...planStepDispatchNotifications(snapshot),
...routeDecisionNotifications(snapshot),
...diagnosticNotifications(snapshot),
...auditNotifications(snapshot),
...taskNotifications(snapshot),
]);
const overlays = state.notificationStateById ?? {};
return notifications
.map((notification) => applyNotificationOverlay(notification, overlays[notification.notification_id]))
.sort((left, right) => right.created_at.localeCompare(left.created_at) || right.notification_id.localeCompare(left.notification_id));
}
function defaultNotificationPreferences(): NotificationPreferences {
return {
enabled: true,
muted_kinds: [],
muted_levels: [],
updated_at: null,
};
}
function defaultApprovalSubscriptionPreferences(): ApprovalSubscriptionPreferences {
return {
enabled: true,
subscribed_actions: [...APPROVAL_SUBSCRIPTION_ACTIONS],
subscribed_risk_levels: ['high', 'critical'],
notify_on_timeout: true,
updated_at: null,
};
}
function defaultPlanStepDispatchPolicy(): PlanStepDispatchPolicy {
return {
enabled: true,
max_per_sweep: 3,
auto_dispatch_ready_steps: true,
updated_at: null,
};
}
function approvalSubscriptionPreferences(state: Partial<AdapterState> | null | undefined): ApprovalSubscriptionPreferences {
const stored = state?.approvalSubscriptionPreferences;
const fallback = defaultApprovalSubscriptionPreferences();
if (!stored || typeof stored !== 'object') return fallback;
const actions = Array.isArray(stored.subscribed_actions)
? stored.subscribed_actions.filter((action): action is ApprovalSubscriptionPreferences['subscribed_actions'][number] => APPROVAL_SUBSCRIPTION_ACTIONS.includes(action as ApprovalSubscriptionPreferences['subscribed_actions'][number]))
: fallback.subscribed_actions;
const riskLevels = Array.isArray(stored.subscribed_risk_levels)
? stored.subscribed_risk_levels.filter((level): level is ApprovalSubscriptionPreferences['subscribed_risk_levels'][number] => APPROVAL_SUBSCRIPTION_RISK_LEVELS.includes(level as ApprovalSubscriptionPreferences['subscribed_risk_levels'][number]))
: fallback.subscribed_risk_levels;
return {
enabled: stored.enabled !== false,
subscribed_actions: [...new Set(actions)],
subscribed_risk_levels: [...new Set(riskLevels)],
notify_on_timeout: stored.notify_on_timeout !== false,
updated_at: typeof stored.updated_at === 'string' ? stored.updated_at : fallback.updated_at,
};
}
function planStepDispatchPolicy(state: Partial<AdapterState> | null | undefined): PlanStepDispatchPolicy {
const stored = state?.planStepDispatchPolicy;
const fallback = defaultPlanStepDispatchPolicy();
if (!stored || typeof stored !== 'object') return fallback;
const maxPerSweep = Number(stored.max_per_sweep);
return {
enabled: stored.enabled !== false,
max_per_sweep: Number.isFinite(maxPerSweep) ? Math.max(1, Math.min(Math.floor(maxPerSweep), 10)) : fallback.max_per_sweep,
auto_dispatch_ready_steps: stored.auto_dispatch_ready_steps !== false,
updated_at: typeof stored.updated_at === 'string' ? stored.updated_at : fallback.updated_at,
};
}
function notificationPreferences(state: Partial<AdapterState> | null | undefined): NotificationPreferences {
const stored = state?.notificationPreferences;
const fallback = defaultNotificationPreferences();
if (!stored || typeof stored !== 'object') return fallback;
const mutedKinds = Array.isArray(stored.muted_kinds)
? stored.muted_kinds.filter((kind): kind is UserNotification['kind'] => NOTIFICATION_KINDS.includes(kind as UserNotification['kind']))
: [];
const mutedLevels = Array.isArray(stored.muted_levels)
? stored.muted_levels.filter((level): level is UserNotification['level'] => NOTIFICATION_LEVELS.includes(level as UserNotification['level']))
: [];
return {
enabled: stored.enabled !== false,
muted_kinds: [...new Set(mutedKinds)],
muted_levels: [...new Set(mutedLevels)],
updated_at: typeof stored.updated_at === 'string' ? stored.updated_at : fallback.updated_at,
};
}
function notificationMatchesPreferences(
notification: UserNotification,
preferences: NotificationPreferences | undefined,
): boolean {
const normalized = notificationPreferences({ notificationPreferences: preferences });
if (!normalized.enabled) return false;
if (normalized.muted_levels.includes(notification.level)) return false;
return !normalized.muted_kinds.includes(notification.kind);
}
async function handleNotificationPreferencesPatch(
patch: Partial<NotificationPreferences>,
snapshot: QimingclawSnapshot | null,
): Promise<NotificationPreferences> {
const state = snapshot?.uiState ?? await readBridgeUiState() ?? readState();
const current = notificationPreferences(state);
const next = notificationPreferences({
notificationPreferences: {
...current,
...patch,
updated_at: new Date().toISOString(),
},
});
await saveState(
{
...normalizeAdapterState(state),
notificationPreferences: next,
},
'update_notification_preferences',
undefined,
{ notification_preferences: next },
);
return next;
}
async function handleApprovalSubscriptionPreferencesPatch(
patch: Partial<ApprovalSubscriptionPreferences>,
snapshot: QimingclawSnapshot | null,
): Promise<ApprovalSubscriptionPreferences> {
const state = snapshot?.uiState ?? await readBridgeUiState() ?? readState();
const current = approvalSubscriptionPreferences(state);
const next = approvalSubscriptionPreferences({
approvalSubscriptionPreferences: {
...current,
...patch,
updated_at: new Date().toISOString(),
},
});
await saveState(
{
...normalizeAdapterState(state),
approvalSubscriptionPreferences: next,
},
'update_approval_subscriptions',
undefined,
{ approval_subscriptions: next },
);
return next;
}
async function handlePlanStepDispatchPolicyPatch(
patch: Partial<PlanStepDispatchPolicy>,
snapshot: QimingclawSnapshot | null,
): Promise<PlanStepDispatchPolicy> {
const state = snapshot?.uiState ?? await readBridgeUiState() ?? readState();
const current = planStepDispatchPolicy(state);
const next = planStepDispatchPolicy({
planStepDispatchPolicy: {
...current,
...patch,
updated_at: new Date().toISOString(),
},
});
await saveState(
{
...normalizeAdapterState(state),
planStepDispatchPolicy: next,
},
'update_plan_step_dispatch_policy',
undefined,
{ plan_step_dispatch_policy: next },
);
return next;
}
function approvalNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] {
const summaries = new Map(businessApprovalRows(snapshot).map((approval) => [stringValue(approval.approval_id), approval]));
return buildApprovals(snapshot)
.filter((approval) => stringValue(approval.source) === 'qimingclaw-runtime')
.map((approval) => {
const status = stringValue(approval.status) || 'pending';
const open = isDecisionOpenStatus(status);
const summary = summaries.get(stringValue(approval.approval_id));
const slaStatus = stringValue(summary?.sla_status);
const riskLevel = stringValue(summary?.risk_level);
const warn = open && (slaStatus === 'timeout' || riskLevel === 'high');
return {
notification_id: `approval:${stringValue(approval.approval_id)}`,
plan_id: stringValue(approval.plan_id),
task_id: stringValue(approval.task_id),
kind: open ? 'need_approval' : 'confirmation_resolved',
title: warn ? '审批需要关注' : open ? '审批待处理' : '审批已处理',
body: stringValue(approval.title) || '待确认事项',
level: warn ? 'warn' : open ? 'action' : 'info',
status: open ? 'unread' : 'resolved',
correlation_id: stringValue(approval.approval_id),
created_at: stringValue(approval.created_at) || new Date().toISOString(),
} satisfies UserNotification;
});
}
function approvalActionNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] {
return approvalActionRows(snapshot)
.filter((action) => stringValue(action.kind) === 'approval_action_recorded')
.filter((action) => ['delegate', 'mark_timeout', 'mark_risk'].includes(stringValue(action.action)))
.map((action) => {
const actionKind = stringValue(action.action);
const approvalId = stringValue(action.approval_id);
const isTimeout = actionKind === 'mark_timeout';
const isRisk = actionKind === 'mark_risk';
return {
notification_id: `approval-action:${approvalId}:${actionKind}:${slugifyIdPart(stringValue(action.occurred_at))}`,
plan_id: stringValue(action.plan_id),
task_id: stringValue(action.task_id),
kind: 'need_approval',
title: isTimeout ? '审批已超时' : isRisk ? '审批已标记风险' : '审批已转交',
body: stringValue(action.message) || approvalId,
level: isTimeout || isRisk ? 'warn' : 'action',
status: 'unread',
correlation_id: approvalId,
created_at: stringValue(action.occurred_at) || new Date().toISOString(),
} satisfies UserNotification;
});
}
function syncFailureNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] {
return (snapshot?.sync?.recentFailures ?? []).map((failure) => ({
notification_id: `sync_failure:${failure.id}`,
plan_id: '',
task_id: failure.entityId,
kind: 'task_failed',
title: `同步失败:${failure.entityType}`,
body: syncFailureLine(failure),
level: failure.dueForRetry === false ? 'error' : 'warn',
status: 'unread',
correlation_id: failure.id,
created_at: failure.lastAttemptAt ?? failure.updatedAt ?? failure.createdAt,
}));
}
function managedServiceNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] {
return buildManagedServices(snapshot)
.filter((service) => service.requires_human_action || service.status === 'degraded' || service.status === 'stopped')
.map((service) => ({
notification_id: `service:${service.service_id}`,
plan_id: '',
task_id: service.dependent_tasks?.[0] ?? '',
kind: 'need_input',
title: `服务需要处理:${service.name}`,
body: service.error || `${service.name} 当前状态:${service.status_label || service.status}`,
level: service.status === 'stopped' ? 'error' : 'warn',
status: 'unread',
correlation_id: service.service_id,
created_at: service.last_seen_at || new Date().toISOString(),
}));
}
function taskNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] {
return runtimeTasks(snapshot).flatMap((task) => {
const normalized = task.status.toLowerCase();
const success = ['completed', 'succeeded', 'success', 'done'].includes(normalized);
const failed = ['failed', 'error', 'rejected', 'cancelled', 'skipped'].includes(normalized);
if (!success && !failed) return [];
const timestamp = task.updatedAt || task.createdAt || new Date().toISOString();
return [{
notification_id: `task:${task.id}:${slugifyIdPart(timestamp)}`,
plan_id: task.planId ?? '',
task_id: task.id,
kind: success ? 'task_finished' : 'task_failed',
title: success ? '任务已完成' : '任务需要关注',
body: `${task.title}${taskStatusLabel(task.status)}`,
level: success ? 'info' : 'error',
status: 'unread',
correlation_id: task.id,
created_at: timestamp,
} satisfies UserNotification];
});
}
function planStepDispatchNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] {
return runtimeEvents(snapshot)
.filter((event) => event.kind === 'plan_step_dispatch_failed')
.map((event) => {
const payload = asRecord(event.payload) ?? {};
const stepId = stringValue(payload.step_id) || event.id;
const reason = stringValue(payload.reason);
return {
notification_id: `plan-step-dispatch:${stepId}:${slugifyIdPart(event.occurredAt)}`,
plan_id: event.planId ?? stringValue(payload.plan_id),
task_id: event.taskId ?? stringValue(payload.task_id),
kind: 'task_failed',
title: 'Ready 步骤派发失败',
body: reason ? `${stepId}${reason}` : (event.message || stepId),
level: 'error',
status: 'unread',
correlation_id: stepId,
created_at: event.occurredAt,
} satisfies UserNotification;
});
}
function routeDecisionNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] {
return routeDecisionRows(snapshot)
.filter((row) => ['warn', 'error', 'action'].includes(stringValue(row.attention_level)))
.map((row) => {
const attentionLevel = stringValue(row.attention_level);
const routeId = stringValue(row.route_decision_id) || stringValue(row.event_id);
const commandId = stringValue(row.created_command_id);
const reasonCodes = arrayValue(row.reason_codes).map(stringValue).filter(Boolean).join(' / ');
return {
notification_id: `route-decision:${routeId}:${slugifyIdPart(stringValue(row.recorded_at))}`,
plan_id: stringValue(asRecord(row.context_refs)?.plan_id),
task_id: stringValue(asRecord(row.context_refs)?.task_id),
kind: attentionLevel === 'action' ? 'task_progress' : 'task_failed',
title: attentionLevel === 'action' ? '入口路由已映射' : '入口路由需要关注',
body: commandId
? `${row.route_kind} / ${row.intent_kind} -> ${commandId}`
: `${row.route_kind} / ${row.intent_kind}${reasonCodes ? `${reasonCodes}` : ''}`,
level: attentionLevel === 'error' ? 'error' : attentionLevel === 'warn' ? 'warn' : 'action',
status: 'unread',
correlation_id: routeId,
created_at: stringValue(row.recorded_at) || new Date().toISOString(),
} satisfies UserNotification;
});
}
function diagnosticNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] {
return buildDoctor(snapshot).results
.filter((result) => result.severity === 'error' || result.severity === 'warn')
.map((result) => {
const level = result.severity === 'error' ? 'error' : 'warn';
const diagnosticId = stringValue(result.id) || `${stringValue(result.category) || 'diagnostic'}:${level}`;
return {
notification_id: `diagnostic:${diagnosticId}`,
plan_id: '',
task_id: '',
kind: level === 'error' ? 'task_failed' : 'need_input',
title: level === 'error' ? '诊断发现异常' : '诊断需要关注',
body: result.message,
level,
status: 'unread',
correlation_id: diagnosticId,
created_at: result.created_at ?? new Date().toISOString(),
} satisfies UserNotification;
});
}
function auditNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] {
return auditRows(snapshot)
.filter((event) => ['error', 'warn'].includes(stringValue(event.level)))
.map((event) => {
const level = stringValue(event.level) === 'error' ? 'error' : 'warn';
const auditId = stringValue(event.audit_id) || stringValue(event.entity_id) || 'audit';
return {
notification_id: `audit:${auditId}`,
plan_id: stringValue(event.plan_id),
task_id: stringValue(event.task_id) || (stringValue(event.entity_type) === 'task' ? stringValue(event.entity_id) : ''),
kind: level === 'error' ? 'task_failed' : 'need_input',
title: level === 'error' ? '审计发现异常' : '审计需要关注',
body: stringValue(event.message) || stringValue(event.action) || auditId,
level,
status: 'unread',
correlation_id: auditId,
created_at: stringValue(event.occurred_at) || new Date().toISOString(),
} satisfies UserNotification;
});
}
function applyNotificationOverlay(notification: UserNotification, overlay?: NotificationState): UserNotification {
if (!overlay) return notification;
return {
...notification,
status: overlay.status ?? notification.status,
read_at: overlay.read_at ?? notification.read_at ?? null,
dismissed_at: overlay.dismissed_at ?? notification.dismissed_at ?? null,
reply: overlay.reply ?? notification.reply ?? null,
replied_at: overlay.replied_at ?? notification.replied_at ?? null,
};
}
function dedupeNotifications(notifications: UserNotification[]): UserNotification[] {
const seen = new Set<string>();
return notifications.filter((notification) => {
if (!notification.notification_id || seen.has(notification.notification_id)) return false;
seen.add(notification.notification_id);
return true;
});
}
async function handleNotificationAction(
notificationId: string,
action: 'read' | 'dismiss' | 'reply',
body: Record<string, unknown>,
snapshot: QimingclawSnapshot | null,
): Promise<{ ok: boolean; task_id?: string; status?: string; task_input_recorded?: boolean; governance_command_id?: string; governance_event_id?: string }> {
const state = snapshot?.uiState ?? await readBridgeUiState() ?? readState();
const current = buildNotifications(snapshot, state).find((notification) => notification.notification_id === notificationId);
const now = new Date().toISOString();
const previousOverlay = state.notificationStateById?.[notificationId];
const alreadyReplied = Boolean(previousOverlay?.replied_at);
const overlay: NotificationState = { ...(previousOverlay ?? {}) };
let replyContent = '';
if (action === 'read') {
overlay.status = current?.status === 'resolved' ? 'resolved' : 'read';
overlay.read_at = overlay.read_at ?? now;
} else if (action === 'dismiss') {
overlay.status = 'dismissed';
overlay.dismissed_at = now;
} else {
overlay.status = current?.status === 'resolved' ? 'resolved' : 'read';
overlay.read_at = overlay.read_at ?? now;
replyContent = stringValue(body.content ?? body.reply ?? body.message);
overlay.reply = replyContent;
overlay.replied_at = now;
}
const nextState: AdapterState = {
...state,
notificationStateById: {
...(state.notificationStateById ?? {}),
[notificationId]: overlay,
},
};
await saveState(nextState, `notification_${action}`, undefined, { notification_id: notificationId, action });
const taskInput = action === 'reply' && current?.kind === 'need_input' && current.task_id && replyContent && !alreadyReplied
? await recordNotificationTaskInput(current, replyContent, snapshot)
: null;
return {
ok: true,
task_id: current?.task_id,
status: overlay.status,
task_input_recorded: Boolean(taskInput?.accepted),
governance_command_id: taskInput?.command_id,
governance_event_id: governanceEventIdFromCommand(taskInput),
};
}
async function recordNotificationTaskInput(
notification: UserNotification,
replyContent: string,
snapshot: QimingclawSnapshot | null,
): Promise<GovernanceCommandResponse> {
const stamp = Date.now();
return handleGovernanceCommand({
command_kind: 'provide_task_input',
context_refs: {
plan_id: notification.plan_id || undefined,
task_id: notification.task_id,
},
payload: {
input: replyContent,
source: 'notification_reply',
notification_id: notification.notification_id,
plan_id: notification.plan_id || undefined,
task_id: notification.task_id,
},
correlation_id: `qimingclaw-notification-reply-${slugifyIdPart(notification.notification_id)}-${stamp}`,
idempotency_key: `qimingclaw-notification-reply-${notification.notification_id}`,
}, snapshot);
}
function governanceEventIdFromCommand(response: GovernanceCommandResponse | null): string | undefined {
const runId = stringFromUnknown(response?.result?.run_id);
if (!response?.accepted || !runId || !response.command_id) return undefined;
return `${runId}:provide_task_input:${safeEventIdPart(response.command_id)}`;
}
function safeEventIdPart(value: string): string {
return value.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'unknown';
}
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 approvalSummaries = new Map(businessApprovalRows(snapshot).map((approval) => [stringValue(approval.approval_id), approval]));
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;
const summary = approvalSummaries.get(decisionId);
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) || '待确认任务',
risk_level: stringValue(summary?.risk_level) || 'normal',
sla_status: stringValue(summary?.sla_status) || 'untracked',
due_at: stringValue(summary?.due_at) || null,
last_action: stringValue(summary?.last_action) || null,
last_actor: stringValue(summary?.last_actor) || null,
comment_count: numberValue(summary?.comment_count) ?? 0,
delegate_to: stringValue(summary?.delegate_to) || null,
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,
})),
];
const deduped = dedupeArtifacts(artifacts);
return deduped.map((artifact) => withArtifactAccessSummary(artifact, snapshot, deduped));
}
function withArtifactAccessSummary(artifact: ArtifactEntry, snapshot: QimingclawSnapshot | null, artifacts: ArtifactEntry[]): ArtifactEntry {
const artifactId = stringValue(artifact.artifact_id) || stringValue(artifact.subject_id);
const lastAccess = artifactAccessRows(snapshot).find((row) => stringValue(row.artifact_id) === artifactId) ?? null;
const hasLocalUri = isLocalArtifactUri(stringValue(artifact.uri));
const hasDailyText = isDailyReportArtifact(artifact) && Boolean(stringValue(asRecord(artifact.payload)?.text_content));
const formalRuntimeArtifact = runtimeArtifacts(snapshot).some((runtime) => runtime.id === artifactId || runtime.remoteId === artifactId);
const previewable = formalRuntimeArtifact && (hasDailyText || hasLocalUri);
const lifecycle = artifactLifecycleSummary(snapshot, artifactId);
const version = artifactVersionMeta(artifacts, artifact);
return {
...artifact,
version: version.version,
version_key: version.version_key,
version_rank: version.version_rank,
retention_status: lifecycle.retention_status,
retention_until: lifecycle.retention_until,
last_retention_action: lifecycle.last_retention_action,
delivery_summary: artifactDeliverySummary(artifact),
access: {
previewable,
downloadable: formalRuntimeArtifact && hasLocalUri,
openable: formalRuntimeArtifact && hasLocalUri,
access_policy: formalRuntimeArtifact ? 'qimingclaw-local-artifact-policy' : 'runtime-projection-readonly',
last_access_status: stringValue(lastAccess?.status) || null,
},
};
}
function artifactFromRuntimeRecord(artifact: QimingclawArtifactRecord): ArtifactEntry {
const payload = asRecord(artifact.payload);
const delivery = asRecord(payload?.delivery);
const sourceTool = stringValue(delivery?.source_tool);
const deliveryUri = stringValue(delivery?.uri);
const deliveryFilename = stringValue(delivery?.filename);
return {
artifact_id: artifact.id,
subject_id: artifact.runId ?? artifact.taskId ?? artifact.planId ?? artifact.id,
kind: artifact.kind,
name: artifact.name || deliveryFilename || stringValue(payload?.name ?? payload?.title) || artifactName(artifact.uri ?? deliveryUri) || artifact.kind,
uri: artifact.uri || deliveryUri || stringValue(payload?.uri ?? payload?.url ?? payload?.path) || undefined,
value: stringValue(payload?.value ?? payload?.summary ?? payload?.text) || artifact.uri || deliveryUri || artifact.name || deliveryFilename || 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: sourceTool ? `File Server: ${sourceTool}` : artifactSourceLabel('artifact', artifact.id),
sync_status: artifact.syncStatus,
sync_error: artifact.syncError,
payload: artifact.payload,
...deliveryProjection(delivery),
};
}
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 delivery = asRecord(record.delivery);
const sourceTool = stringValue(delivery?.source_tool);
const deliveryUri = stringValue(delivery?.uri);
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) || stringValue(delivery?.filename) || artifactName(uri || deliveryUri) || `产物 ${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: uri || deliveryUri,
value: stringValue(record.value ?? record.summary ?? record.text) || uri || deliveryUri,
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: sourceTool ? `File Server: ${sourceTool}` : 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,
...deliveryProjection(delivery),
};
}
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 deliveryProjection(delivery: Record<string, unknown> | null): Record<string, unknown> {
if (!delivery) return {};
return {
delivery_status: stringValue(delivery.status) || null,
delivery_stage: stringValue(delivery.stage) || null,
workspace_id: stringValue(delivery.workspace_id) || null,
project_id: stringValue(delivery.project_id) || null,
file_id: stringValue(delivery.file_id) || null,
version: stringValue(delivery.version) || null,
file_size: numberValue(delivery.size),
source_tool: stringValue(delivery.source_tool) || 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 numberValue(value: unknown): number | null {
if (typeof value === 'number' && Number.isFinite(value)) return value;
if (typeof value === 'string' && value.trim()) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
return null;
}
function looksLikeUri(value: string): boolean {
return /^(file|https?):\/\//i.test(value) || value.includes('/') || value.includes('\\');
}
function isLocalArtifactUri(value: string): boolean {
if (!value) return false;
if (/^file:\/\//i.test(value)) return true;
if (/^[a-z][a-z0-9+.-]*:/i.test(value)) return false;
return value.startsWith('/') || /^[a-z]:[\\/]/i.test(value);
}
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 runDetails = buildRunDetails(events);
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: runDetails,
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,
})),
],
},
route_summary: buildRouteDecisionSummary(snapshot),
diagnostic_summary: buildDiagnosticSummary(snapshot),
daily_report: buildDailyReportProjection({ date, state, generatedAt, selectedCount, events, executionSummaries, runDetails, artifactSources, successCount, failureCount, runningCount, snapshot }),
};
}
function buildDailyReportProjection(input: {
date: string;
state: AdapterState;
generatedAt: string | null;
selectedCount: number;
events: DigitalEmployeeWorkEvent[];
executionSummaries: DigitalEmployeeTaskExecutionSummary[];
runDetails: DigitalEmployeeRunDetail[];
artifactSources: ReturnType<typeof reportArtifactSources>;
successCount: number;
failureCount: number;
runningCount: number;
snapshot: QimingclawSnapshot | null;
}): DigitalEmployeeDailyReport {
const title = `${displayDate(input.date)} 飞天数字员工日报`;
const summary = input.generatedAt
? `今日已完成数字员工页面到 qimingclaw adapter 的接入,并读取了 qimingclaw 服务状态。${syncLine(input.snapshot)}`
: '日报将在生成后汇总任务选择、运行明细和后续接入建议。';
const totals = {
task_count: input.selectedCount,
execution_count: input.events.length,
success_count: input.successCount,
failure_count: input.failureCount,
running_count: input.runningCount,
};
const detailLines = input.events.map((event) => `${event.time} ${event.task_title}${event.detail}`);
const reportId = input.generatedAt ? dailyReportId(input.date, input.generatedAt) : null;
const downloadFilename = dailyReportFilename(input.date);
const baseReport: DigitalEmployeeDailyReport = {
report_id: reportId,
artifact_id: reportId ? `${reportId}:artifact:text` : null,
event_id: reportId ? `${reportId}:event:generated` : null,
date: input.date,
title,
report_schedule_time: input.state.reportScheduleTime,
status: input.generatedAt ? 'ready' : 'not_generated',
generated_at: input.generatedAt,
generated_by: input.generatedAt ? 'ai' : 'fallback',
model: 'qimingclaw-adapter',
source: 'qimingclaw',
summary,
totals,
task_sections: input.executionSummaries,
detail_lines: detailLines,
artifact_sources: input.artifactSources,
download_filename: downloadFilename,
text_content: null,
pdf_available: Boolean(input.generatedAt),
pdf_url: null,
};
return {
...baseReport,
text_content: input.generatedAt
? buildDailyReportText(baseReport, input.runDetails, input.artifactSources)
: null,
};
}
function buildDailyReportText(
report: DigitalEmployeeDailyReport,
details: DigitalEmployeeRunDetail[],
artifacts: ReturnType<typeof reportArtifactSources>,
): string {
return [
report.title || `${report.date} 数字员工日报`,
`日期:${report.date}`,
`生成时间:${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}`,
'',
'## 任务汇总',
...(report.task_sections.length > 0
? report.task_sections.map((section) => `- ${section.title}${section.latest_status_label},执行 ${section.total_count} 次,成功 ${section.success_count},失败 ${section.failure_count}${section.ai_summary || section.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}`)
: ['暂无运行明细。']),
'',
'## 产物来源',
...(artifacts.length > 0
? artifacts.map((artifact) => `- ${artifact.name}${artifact.source_label || artifact.source_type || '未知来源'}${artifact.uri ? `${artifact.uri}` : ''}`)
: ['暂无产物来源。']),
'',
'## 详情',
...((report.detail_lines ?? []).length > 0 ? report.detail_lines : ['暂无详情。']),
].join('\n');
}
function dailyReportFilename(date: string): string {
return `qimingclaw-digital-report-${date}.txt`;
}
function dailyReportId(date: string, generatedAt: string): string {
return `daily-report:${date}:${slugifyIdPart(generatedAt)}`;
}
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,
delivery_status: stringValue(artifact.delivery_status) || null,
delivery_stage: stringValue(artifact.delivery_stage) || null,
workspace_id: stringValue(artifact.workspace_id) || null,
project_id: stringValue(artifact.project_id) || null,
file_id: stringValue(artifact.file_id) || null,
version: stringValue(artifact.version) || null,
version_key: stringValue(artifact.version_key) || null,
version_rank: numberValue(artifact.version_rank),
retention_status: stringValue(artifact.retention_status) || null,
retention_until: stringValue(artifact.retention_until) || null,
last_retention_action: stringValue(artifact.last_retention_action) || null,
file_size: typeof artifact.file_size === 'number' ? artifact.file_size : null,
source_tool: stringValue(artifact.source_tool) || 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,
access: artifact.access,
delivery_summary: artifact.delivery_summary ?? 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,
);
const nextSnapshot = { ...(snapshot ?? { generatedAt: new Date().toISOString(), services: {}, sessions: { total: 0, active: 0, items: [] } }), uiState: savedState };
const projection = buildWorkday(date, nextSnapshot);
if (body.action === 'generate_daily_report' && projection.daily_report) {
await recordDailyReportArtifact(projection.daily_report);
}
return projection;
}
async function recordDailyReportArtifact(report: DigitalEmployeeDailyReport): Promise<void> {
if (!report.report_id || !report.generated_at || !report.text_content) return;
try {
await window.QimingClawBridge?.digital?.recordDailyReport?.({
reportId: report.report_id,
date: report.date,
title: report.title,
summary: report.summary,
totals: report.totals,
detailLines: report.detail_lines,
artifactSources: report.artifact_sources ?? [],
filename: report.download_filename || dailyReportFilename(report.date),
textContent: report.text_content,
generatedAt: report.generated_at,
reportScheduleTime: report.report_schedule_time,
source: report.source ?? 'qimingclaw',
model: report.model ?? 'qimingclaw-adapter',
});
} catch (error) {
console.warn('[qimingclawAdapter] record daily report failed:', error);
}
}
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 handlePlanAction(
planId: string,
action: string,
snapshot: QimingclawSnapshot | null,
payload: Record<string, unknown> = {},
): Promise<GovernanceCommandResponse> {
const commandKind = planActionCommandKind(action);
const correlationId = `plan-action-${slugifyIdPart(planId)}-${slugifyIdPart(action)}-${Date.now()}`;
if (!commandKind) {
return {
command_id: `qimingclaw-digital-plan-action-${Date.now()}`,
accepted: false,
correlation_id: correlationId,
message: `暂不支持计划动作:${action}`,
error: 'unsupported_command',
};
}
return handleGovernanceCommand({
command_kind: commandKind,
context_refs: { plan_id: planId },
payload: {
...payload,
plan_id: planId,
requested_action: `plan_${action}`,
action_endpoint: `/api/debug/plan/${planId}/${action}`,
},
correlation_id: correlationId,
}, snapshot);
}
async function handleTaskAction(
taskId: string,
action: string,
snapshot: QimingclawSnapshot | null,
payload: Record<string, unknown> = {},
): Promise<GovernanceCommandResponse> {
const commandKind = taskActionCommandKind(action);
const task = runtimeTasks(snapshot).find((candidate) => candidate.id === taskId);
const latestRun = latestRunForTask(runtimeRuns(snapshot), taskId);
const planId = task?.planId ?? (stringValue(payload.plan_id) || undefined);
const runId = latestRun?.id ?? (stringValue(payload.run_id) || undefined);
const stepId = task ? taskStepId(task) : (stringValue(payload.step_id) || undefined);
const correlationId = `task-action-${slugifyIdPart(taskId)}-${slugifyIdPart(action)}-${Date.now()}`;
if (!commandKind) {
return {
command_id: `qimingclaw-digital-task-action-${Date.now()}`,
accepted: false,
correlation_id: correlationId,
message: `暂不支持任务动作:${action}`,
error: 'unsupported_command',
};
}
return handleGovernanceCommand({
command_kind: commandKind,
context_refs: {
plan_id: planId,
task_id: taskId,
run_id: runId,
step_id: stepId,
},
payload: {
...payload,
task_id: taskId,
plan_id: planId ?? payload.plan_id,
run_id: runId ?? payload.run_id,
step_id: stepId ?? payload.step_id,
requested_action: `task_${action}`,
action_endpoint: `/api/task/${taskId}/${action}`,
},
correlation_id: correlationId,
}, snapshot);
}
function planActionCommandKind(action: string): GovernanceCommandRequest['command_kind'] | null {
switch (action.toLowerCase()) {
case 'submit':
return 'submit_plan';
case 'approve':
return 'approve_plan';
case 'activate':
return 'activate_plan';
case 'pause':
return 'pause_plan';
case 'resume':
return 'resume_plan';
case 'cancel':
return 'cancel_plan';
case 'amend':
return 'amend_plan';
default:
return null;
}
}
function taskActionCommandKind(action: string): GovernanceCommandRequest['command_kind'] | null {
switch (action.toLowerCase()) {
case 'retry':
return 'retry_task';
case 'skip':
return 'skip_task';
case 'pause':
return 'pause_task';
case 'resume':
return 'resume_task';
case 'cancel':
return 'cancel_task';
case 'start':
return 'start_run';
default:
return null;
}
}
function buildPlanRevisions(planId: string, snapshot: QimingclawSnapshot | null): { plan_id: string; revisions: Array<Record<string, unknown>> } {
const revisions = runtimeEvents(snapshot)
.filter((event) => event.planId === planId && event.kind.startsWith('governance_'))
.sort((left, right) => right.occurredAt.localeCompare(left.occurredAt) || right.createdAt.localeCompare(left.createdAt))
.map((event) => {
const payload = asRecord(event.payload) ?? {};
const result = asRecord(payload.result) ?? {};
const commandKind = stringValue(payload.commandKind) || event.kind.replace(/^governance_/, '');
return {
revision_id: event.id,
plan_id: planId,
event_id: event.id,
command_kind: commandKind,
action: commandKind.replace(/_plan$/, ''),
status: stringValue(result.status) || stringValue(payload.status) || 'recorded',
message: event.message,
created_at: event.occurredAt,
payload,
};
});
return { plan_id: planId, revisions };
}
async function readRouteDecisions(limit = 80): Promise<RouteDecisionTimelineResponse> {
const bridge = window.QimingClawBridge?.digital;
if (!bridge?.listRouteDecisions) {
return { has_route_decision_state: false, decisions: [] };
}
try {
const decisions = await bridge.listRouteDecisions({ limit });
return { has_route_decision_state: true, decisions: decisions ?? [] };
} catch (error) {
console.warn('[qimingclawAdapter] list route decisions failed:', error);
return { has_route_decision_state: false, decisions: [] };
}
}
async function handleIngressRoute(
body: IngressRouteRequest,
snapshot: QimingclawSnapshot | null,
): Promise<IngressRouteResponse> {
const recordedAt = body.timestamp || new Date().toISOString();
const correlationId = body.correlation_id || `route-${Date.now()}`;
const idSource = body.idempotency_key || correlationId;
const contextRefs = buildIngressContextRefs(body);
const classification = classifyIngressRoute(body, contextRefs, snapshot);
const filteredCandidates = await filterRouteCandidateSkills(classification.candidateSkills);
const reasonCodes = filteredCandidates.allCandidatesDisabled
? [...classification.reasonCodes, 'candidate_skills_disabled']
: classification.reasonCodes;
const decision: RouteDecisionRecord = {
id: `route-decision-${slugifyIdPart(idSource)}`,
route_kind: classification.routeKind,
intent_kind: classification.intentKind,
reason_codes: reasonCodes,
candidate_skills: filteredCandidates.candidateSkills,
context_refs: contextRefs,
created_command_id: null,
correlation_id: correlationId,
creates_plan: classification.createsPlan,
creates_task_run: classification.createsTaskRun,
approval_mode: null,
policy_result: 'accepted',
recorded_at: recordedAt,
entrypoint: body.entrypoint ?? null,
actor: body.actor ?? null,
};
const routeCommand = buildGovernanceCommandForRouteDecision(decision, body, snapshot);
const command = routeCommand.command
? await handleGovernanceCommand(routeCommand.command, snapshot)
: null;
if (command?.accepted && command.command_id) {
decision.created_command_id = command.command_id;
decision.reason_codes = uniqueStrings([...decision.reason_codes, 'route_action_mapped']);
} else if (routeCommand.reasonCode) {
decision.reason_codes = uniqueStrings([...decision.reason_codes, routeCommand.reasonCode]);
}
const bridge = window.QimingClawBridge?.digital;
const recorded = bridge?.recordRouteDecision
? await bridge.recordRouteDecision({
id: decision.id,
idempotencyKey: body.idempotency_key,
correlationId,
routeKind: decision.route_kind,
intentKind: decision.intent_kind,
reasonCodes: decision.reason_codes,
candidateSkills: decision.candidate_skills,
contextRefs: decision.context_refs,
createdCommandId: decision.created_command_id,
createsPlan: decision.creates_plan,
createsTaskRun: decision.creates_task_run,
approvalMode: decision.approval_mode,
policyResult: decision.policy_result,
entrypoint: decision.entrypoint,
actor: decision.actor,
message: `入口路由决策:${decision.route_kind} / ${decision.intent_kind}`,
envelope: {
text: body.text ?? null,
payload: body.payload ?? null,
attachments: body.attachments ?? [],
},
recordedAt,
})
: null;
const routeDecision = recorded ?? decision;
const resultIds = routeCommandResultIds(command);
return {
accepted: true,
envelope: {
entrypoint: body.entrypoint ?? 'chat',
text: body.text ?? null,
payload: body.payload ?? null,
attachments: body.attachments ?? [],
context_refs: contextRefs,
},
response_kind: 'route_decision',
route_decision: routeDecision,
command,
created_plan_id: resultIds.createdPlanId,
created_schedule_id: resultIds.createdScheduleId,
created_task_ids: resultIds.createdTaskIds,
created_run_ids: resultIds.createdRunIds,
projection_name: null,
projection_payload: null,
message: command?.command_id
? `已记录入口路由并映射治理动作:${routeDecision.route_kind} / ${routeDecision.intent_kind}`
: `已记录入口路由:${routeDecision.route_kind} / ${routeDecision.intent_kind}`,
};
}
function buildGovernanceCommandForRouteDecision(
decision: RouteDecisionRecord,
body: IngressRouteRequest,
snapshot: QimingclawSnapshot | null,
): { command: GovernanceCommandRequest | null; reasonCode: string } {
if (decision.reason_codes.includes('candidate_skills_disabled')) {
return { command: null, reasonCode: 'route_action_not_mapped' };
}
const contextRefs = decision.context_refs ?? {};
const payload = asRecord(body.payload) ?? {};
const basePayload = compactRecord({
...payload,
source: 'qimingclaw-ingress-route',
route_decision_id: decision.id,
route_kind: decision.route_kind,
intent_kind: decision.intent_kind,
reason_codes: decision.reason_codes,
ingress_text: body.text ?? null,
});
const baseCommand = (commandKind: GovernanceCommandRequest['command_kind'], refs: Record<string, unknown>): GovernanceCommandRequest => ({
command_kind: commandKind,
context_refs: compactRecord({ ...contextRefs, ...refs }),
payload: compactRecord({ ...basePayload, ...refs }),
correlation_id: decision.correlation_id,
idempotency_key: `route-command:${decision.id}`,
});
if (decision.route_kind === 'PlanExecution' && decision.intent_kind === 'schedule') {
const scheduleId = stringValue(contextRefs.schedule_id) || stringValue(payload.schedule_id) || stringValue(payload.scheduleId);
if (!scheduleId) return { command: null, reasonCode: 'route_action_missing_context' };
return { command: baseCommand('run_schedule_now', { schedule_id: scheduleId }), reasonCode: 'route_action_mapped' };
}
if (decision.route_kind === 'PlanExecution' && decision.intent_kind === 'execute') {
const stepId = stringValue(contextRefs.step_id) || stringValue(payload.step_id) || stringValue(payload.stepId);
const taskId = stringValue(contextRefs.task_id) || stringValue(payload.task_id) || stringValue(payload.taskId) || taskIdForRouteStep(snapshot, stepId);
if (!taskId) return { command: null, reasonCode: 'route_action_missing_context' };
const task = runtimeTasks(snapshot).find((candidate) => candidate.id === taskId) ?? null;
return {
command: baseCommand('start_run', {
plan_id: stringValue(contextRefs.plan_id) || task?.planId || stringValue(payload.plan_id) || stringValue(payload.planId),
task_id: taskId,
step_id: stepId || (task ? taskStepId(task) : ''),
run_id: stringValue(contextRefs.run_id) || stringValue(payload.run_id) || stringValue(payload.runId),
}),
reasonCode: 'route_action_mapped',
};
}
if (decision.route_kind === 'PlanControl' && decision.intent_kind === 'control') {
const actionText = routeActionText(body);
const taskId = stringValue(contextRefs.task_id) || stringValue(payload.task_id) || stringValue(payload.taskId);
const planId = stringValue(contextRefs.plan_id) || stringValue(payload.plan_id) || stringValue(payload.planId);
const commandKind = routeControlCommandKind(actionText, Boolean(taskId));
if (!commandKind || (!taskId && !planId)) return { command: null, reasonCode: 'route_action_missing_context' };
return {
command: baseCommand(commandKind, {
plan_id: planId,
task_id: taskId,
step_id: stringValue(contextRefs.step_id) || stringValue(payload.step_id) || stringValue(payload.stepId),
run_id: stringValue(contextRefs.run_id) || stringValue(payload.run_id) || stringValue(payload.runId),
}),
reasonCode: 'route_action_mapped',
};
}
return { command: null, reasonCode: 'route_action_not_mapped' };
}
function taskIdForRouteStep(snapshot: QimingclawSnapshot | null, stepId: string): string {
if (!stepId) return '';
const ready = runtimePlanSteps(snapshot).some((step) => step.id === stepId && step.status.toLowerCase() === 'ready');
if (!ready) return '';
return runtimeTasks(snapshot).find((task) => taskStepId(task) === stepId)?.id ?? '';
}
function routeActionText(body: IngressRouteRequest): string {
const payload = asRecord(body.payload) ?? {};
return [body.text, payload.action, payload.command, payload.intent, payload.requested_action]
.map(stringValue)
.filter(Boolean)
.join(' ')
.toLowerCase();
}
function routeControlCommandKind(actionText: string, hasTaskTarget: boolean): GovernanceCommandRequest['command_kind'] | null {
if (hasTaskTarget) {
if (matchesRouteText(actionText, ['retry', '重试'])) return 'retry_task';
if (matchesRouteText(actionText, ['skip', '跳过'])) return 'skip_task';
if (matchesRouteText(actionText, ['pause', '暂停'])) return 'pause_task';
if (matchesRouteText(actionText, ['resume', '恢复'])) return 'resume_task';
if (matchesRouteText(actionText, ['cancel', '取消'])) return 'cancel_task';
if (matchesRouteText(actionText, ['start', 'run', 'execute', 'begin', '启动', '开始', '执行'])) return 'start_run';
return null;
}
if (matchesRouteText(actionText, ['submit', '提交'])) return 'submit_plan';
if (matchesRouteText(actionText, ['approve', '批准', '同意'])) return 'approve_plan';
if (matchesRouteText(actionText, ['pause', '暂停'])) return 'pause_plan';
if (matchesRouteText(actionText, ['resume', '恢复'])) return 'resume_plan';
if (matchesRouteText(actionText, ['cancel', '取消'])) return 'cancel_plan';
if (matchesRouteText(actionText, ['amend', 'edit', 'revise', '修改', '调整'])) return 'amend_plan';
if (matchesRouteText(actionText, ['activate', 'start', 'run', 'execute', '启动', '开始', '执行'])) return 'activate_plan';
return null;
}
function routeCommandResultIds(command: GovernanceCommandResponse | null): {
createdPlanId: string | null;
createdScheduleId: string | null;
createdTaskIds: string[];
createdRunIds: string[];
} {
const result = asRecord(command?.result) ?? {};
const createdTaskIds = uniqueStrings([
result.task_id,
result.taskId,
...arrayValue(result.dispatched_tasks).map((task) => asRecord(task)?.task_id ?? asRecord(task)?.taskId),
]);
return {
createdPlanId: stringValue(result.plan_id) || stringValue(result.planId) || null,
createdScheduleId: stringValue(result.schedule_id) || stringValue(result.scheduleId) || null,
createdTaskIds,
createdRunIds: uniqueStrings([result.run_id, result.runId]),
};
}
function buildIngressContextRefs(body: IngressRouteRequest): Record<string, unknown> {
const payload = asRecord(body.payload) ?? {};
const context = asRecord(body.context_refs) ?? {};
return compactRecord({
...context,
entrypoint: body.entrypoint ?? context.entrypoint,
plan_id: stringValue(context.plan_id) || stringValue(payload.plan_id) || stringValue(payload.planId),
task_id: stringValue(context.task_id) || stringValue(payload.task_id) || stringValue(payload.taskId),
run_id: stringValue(context.run_id) || stringValue(payload.run_id) || stringValue(payload.runId),
step_id: stringValue(context.step_id) || stringValue(payload.step_id) || stringValue(payload.stepId),
schedule_id: stringValue(context.schedule_id) || stringValue(payload.schedule_id) || stringValue(payload.scheduleId),
});
}
function classifyIngressRoute(
body: IngressRouteRequest,
contextRefs: Record<string, unknown>,
snapshot: QimingclawSnapshot | null,
): { routeKind: string; intentKind: string; reasonCodes: string[]; candidateSkills: string[]; createsPlan: boolean; createsTaskRun: boolean } {
const payload = asRecord(body.payload) ?? {};
const text = `${body.text ?? ''} ${stringValue(payload.action)} ${stringValue(payload.command)} ${stringValue(payload.intent)}`.toLowerCase();
const hasPayload = Object.keys(payload).length > 0;
const hasContext = ['plan_id', 'task_id', 'step_id', 'schedule_id'].some((key) => Boolean(stringValue(contextRefs[key])));
const stepId = stringValue(contextRefs.step_id);
const readyStep = stepId && runtimePlanSteps(snapshot).some((step) => step.id === stepId && step.status.toLowerCase() === 'ready');
if (hasContext && matchesRouteText(text, ['start', 'run', 'retry', 'skip', 'cancel', 'pause', 'resume', 'approve', '启动', '开始', '执行', '重试', '跳过', '取消', '暂停', '恢复', '批准'])) {
return routeClassification('PlanControl', 'control', ['context_control_action'], contextRefs, payload, false, false);
}
if (body.entrypoint === 'cron' || matchesRouteText(text, ['cron', 'schedule', 'scheduled', '定时', '调度', '周期'])) {
return routeClassification('PlanExecution', 'schedule', ['scheduled_entrypoint'], contextRefs, payload, false, true);
}
if (matchesRouteText(text, ['status', 'progress', 'result', 'report', 'artifact', 'memory', '状态', '进度', '结果', '日报', '产物', '记忆'])) {
return routeClassification('ProjectionQuery', 'query', ['projection_query_text'], contextRefs, payload, false, false);
}
if (readyStep || matchesRouteText(text, ['execute', 'dispatch', 'delegate', 'run task', '执行', '开始', '委托', '派发'])) {
return routeClassification('PlanExecution', 'execute', readyStep ? ['ready_step_context'] : ['execution_text'], contextRefs, payload, false, true);
}
if (!body.text && !hasPayload && !hasContext) {
return routeClassification('AskClarification', 'chat', ['empty_ingress'], contextRefs, payload, false, false);
}
return routeClassification('ChatOnly', 'chat', ['default_chat'], contextRefs, payload, false, false);
}
function routeClassification(
routeKind: string,
intentKind: string,
reasonCodes: string[],
contextRefs: Record<string, unknown>,
payload: Record<string, unknown>,
createsPlan: boolean,
createsTaskRun: boolean,
): { routeKind: string; intentKind: string; reasonCodes: string[]; candidateSkills: string[]; createsPlan: boolean; createsTaskRun: boolean } {
return {
routeKind,
intentKind,
reasonCodes,
candidateSkills: routeCandidateSkills(routeKind, contextRefs, payload),
createsPlan,
createsTaskRun,
};
}
function routeCandidateSkills(routeKind: string, contextRefs: Record<string, unknown>, payload: Record<string, unknown>): string[] {
const explicit = uniqueStrings([
...arrayValue(contextRefs.skill_ids),
...arrayValue(contextRefs.skills),
...arrayValue(contextRefs.candidate_skills),
...arrayValue(payload.skill_ids),
...arrayValue(payload.skills),
...arrayValue(payload.candidate_skills),
stringValue(contextRefs.skill_id),
stringValue(payload.skill_id),
]);
if (explicit.length > 0) return explicit;
if (routeKind === 'PlanControl') return ['qimingclaw-acp-session'];
if (routeKind === 'ProjectionQuery') return ['qimingclaw-artifact-reporting'];
if (routeKind === 'PlanExecution') return ['qimingclaw-computer-control'];
return [];
}
async function filterRouteCandidateSkills(
candidateSkills: string[],
): Promise<{ candidateSkills: string[]; allCandidatesDisabled: boolean }> {
if (candidateSkills.length === 0) {
return { candidateSkills, allCandidatesDisabled: false };
}
const bridge = window.QimingClawBridge?.digital;
if (!bridge?.getSkillCapabilities) {
return { candidateSkills, allCandidatesDisabled: false };
}
try {
const result = await bridge.getSkillCapabilities();
if (!Array.isArray(result?.skills)) {
return { candidateSkills, allCandidatesDisabled: false };
}
const enabledBySkillId = new Map<string, boolean>();
for (const skill of result.skills) {
const skillId = stringValue(skill.skill_id);
if (skillId) enabledBySkillId.set(skillId, skill.enabled !== false);
}
const enabledCandidates = candidateSkills.filter((skillId) => enabledBySkillId.get(skillId) !== false);
return {
candidateSkills: enabledCandidates,
allCandidatesDisabled: candidateSkills.length > 0 && enabledCandidates.length === 0,
};
} catch (error) {
console.warn('[qimingclawAdapter] filter route candidate skills failed:', error);
return { candidateSkills, allCandidatesDisabled: false };
}
}
function uniqueStrings(values: unknown[]): string[] {
return [...new Set(values.map(stringValue).filter(Boolean))];
}
function matchesRouteText(text: string, keywords: string[]): boolean {
return keywords.some((keyword) => text.includes(keyword.toLowerCase()));
}
function compactRecord(record: Record<string, unknown>): Record<string, unknown> {
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && value !== null && value !== ''));
}
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 runId = stringFromUnknown(body.context_refs?.run_id)
|| stringFromUnknown(body.payload?.run_id)
|| stringFromUnknown(body.payload?.latest_run_id);
const stepId = stringFromUnknown(body.context_refs?.step_id)
|| stringFromUnknown(body.payload?.step_id)
|| stringFromUnknown(body.payload?.stepId);
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,
schedule_id: body.command_kind === 'run_schedule_now' ? scheduleId : null,
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 runtimeTask = runtimeTasks(snapshot).find((task) => task.id === targetTask);
const targetRunId = runId || latestRunForTask(runtimeRuns(snapshot), targetTask)?.id || null;
const targetStepId = stepId || (runtimeTask ? taskStepId(runtimeTask) : null);
const response = governanceAccepted(commandId, correlationId, `已接受 ${body.command_kind},并记录到 qimingclaw Task Agent 控制平面。`, {
plan_id: planId || runtimeTask?.planId || 'qimingclaw-task-agent-plan',
task_id: targetTask,
run_id: targetRunId,
step_id: targetStepId,
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,
};
}
async function dispatchPlanReadySteps(
planId: string,
snapshot: QimingclawSnapshot | null = null,
): Promise<ReadyStepDispatchResult> {
const targetPlanId = stringValue(planId);
const bridgeResult = targetPlanId ? await window.QimingClawBridge?.digital?.dispatchPlanReadySteps?.(targetPlanId).catch(() => null) : null;
if (bridgeResult) return bridgeResult;
const now = new Date().toISOString();
const orchestrationId = `manual-ready-steps-${slugifyIdPart(targetPlanId || 'unknown')}-${slugifyIdPart(now)}`;
const rows = planStepRows(snapshot).filter((row) => stringValue(row.plan_id) === targetPlanId && stringValue(row.status).toLowerCase() === 'ready');
const state = notificationAdapterState(snapshot);
const policy = planStepDispatchPolicy(state);
const result: ReadyStepDispatchResult = {
ok: true,
plan_id: targetPlanId || null,
trigger_source: 'manual',
orchestration_id: orchestrationId,
candidates: rows.length,
dispatched: 0,
skipped: 0,
failed: 0,
dispatched_step_ids: [],
skipped_step_ids: [],
failed_step_ids: [],
errors: [],
};
if (!targetPlanId || !policy.enabled) {
result.skipped = rows.length;
result.skipped_step_ids = rows.map((row) => stringValue(row.step_id)).filter(Boolean);
return result;
}
const maxPerSweep = Math.max(1, Math.min(Math.floor(policy.max_per_sweep), 10));
let attempts = 0;
for (const row of rows) {
const stepId = stringValue(row.step_id);
const taskId = stringValue(row.task_id);
if (attempts >= maxPerSweep || row.dispatchable !== true || !taskId) {
result.skipped += 1;
if (stepId) result.skipped_step_ids?.push(stepId);
continue;
}
attempts += 1;
try {
const runId = stringValue(row.latest_run_id) || `${taskId}:run:start_run:${slugifyIdPart(orchestrationId)}`;
const commandId = `manual-ready-step-${slugifyIdPart(stepId)}-${Date.now()}`;
await recordGovernanceCommand(
{
command_kind: 'start_run',
context_refs: { plan_id: targetPlanId, task_id: taskId, run_id: runId, step_id: stepId },
payload: {
source: 'digital-home-plan-orchestration',
plan_id: targetPlanId,
task_id: taskId,
run_id: runId,
step_id: stepId,
trigger_source: 'manual',
orchestration_id: orchestrationId,
candidate_count: rows.length,
},
correlation_id: commandId,
},
governanceAccepted(commandId, commandId, '已手动推进 Ready PlanStep。', {
plan_id: targetPlanId,
task_id: taskId,
run_id: runId,
step_id: stepId,
status: 'running',
trigger_source: 'manual',
orchestration_id: orchestrationId,
source: 'qimingclaw-plan-orchestration',
}),
);
result.dispatched += 1;
if (stepId) result.dispatched_step_ids?.push(stepId);
} catch (error) {
result.failed += 1;
if (stepId) result.failed_step_ids?.push(stepId);
result.errors?.push({ stepId, error: error instanceof Error ? error.message : 'dispatch failed' });
}
}
return result;
}
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(),
},
];
}