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