吸收数字员工业务查询视图

This commit is contained in:
baiyanyun
2026-06-09 21:25:03 +08:00
parent d1ae9421c4
commit ba806f779e
5 changed files with 404 additions and 152 deletions

View File

@@ -573,6 +573,11 @@ const PLAN_SCHEDULES: Record<string, { expression: string; nextRun: string; sche
}, },
}; };
const BUSINESS_VIEW_SOURCE = 'qimingclaw-runtime-projection';
type BusinessViewKind = 'plans' | 'tasks' | 'runs' | 'events' | 'artifacts' | 'approvals';
type BusinessViewRow = Record<string, unknown>;
export function isQimingclawDigitalApiEnabled(): boolean { export function isQimingclawDigitalApiEnabled(): boolean {
return window.__QIMINGCLAW_EMBEDDED_DIGITAL__ === true; return window.__QIMINGCLAW_EMBEDDED_DIGITAL__ === true;
} }
@@ -593,6 +598,17 @@ export async function handleQimingclawDigitalApi<T>(
if (method === 'POST' && pathname === '/api/digital-employee/workday/actions') { if (method === 'POST' && pathname === '/api/digital-employee/workday/actions') {
return await handleWorkdayAction(parseJsonBody<DigitalEmployeeWorkdayActionRequest>(options.body), await readQimingclawSnapshot()) as T; return await handleWorkdayAction(parseJsonBody<DigitalEmployeeWorkdayActionRequest>(options.body), await readQimingclawSnapshot()) 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 businessViewMatch = pathname.match(/^\/api\/digital-employee\/(plans|tasks|runs|events|artifacts|approvals)$/);
if (method === 'GET' && businessViewMatch) {
return buildBusinessViewResponse(businessViewMatch[1] as BusinessViewKind, await readQimingclawSnapshot(), url.searchParams) as T;
}
if (method === 'GET' && pathname === '/api/route-decisions') { if (method === 'GET' && pathname === '/api/route-decisions') {
return await readRouteDecisions(Number(url.searchParams.get('limit')) || 80) as T; return await readRouteDecisions(Number(url.searchParams.get('limit')) || 80) as T;
} }
@@ -2050,6 +2066,242 @@ function buildAuditEvents(snapshot: QimingclawSnapshot | null, searchParams: URL
.slice(0, limit); .slice(0, limit);
} }
function buildBusinessViewResponse(kind: BusinessViewKind, snapshot: QimingclawSnapshot | null, searchParams: URLSearchParams): Record<string, unknown> {
const rowsByKind: Record<BusinessViewKind, BusinessViewRow[]> = {
plans: businessPlanRows(snapshot),
tasks: businessTaskRows(snapshot),
runs: businessRunRows(snapshot),
events: businessEventRows(snapshot),
artifacts: businessArtifactRows(snapshot),
approvals: businessApprovalRows(snapshot),
};
const rows = filterBusinessRows(rowsByKind[kind], searchParams);
return {
...paginateBusinessRows(rows, searchParams),
source: BUSINESS_VIEW_SOURCE,
};
}
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]));
return buildArtifacts(snapshot).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,
});
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,
};
});
}
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 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,
};
});
}
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.artifact_id,
row.approval_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> { function auditEventFromRuntimeEvent(event: QimingclawEventRecord): Record<string, unknown> {
const payload = asRecord(event.payload) ?? {}; const payload = asRecord(event.payload) ?? {};
const category = event.kind.startsWith('governance_') const category = event.kind.startsWith('governance_')

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -16,7 +16,7 @@
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error); console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
} }
</script> </script>
<script type="module" crossorigin src="./assets/index-BgNhwaUy.js"></script> <script type="module" crossorigin src="./assets/index-DGRcTTE8.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-BarRiSjT.css"> <link rel="stylesheet" crossorigin href="./assets/index-BarRiSjT.css">
</head> </head>
<body> <body>

View File

@@ -599,9 +599,9 @@ GET /api/digital-employee/approvals
- 建议:下一轮围绕 sandbox audit 与 token/cost 采集,把只读投影升级为可追溯的真实审计与成本统计。 - 建议:下一轮围绕 sandbox audit 与 token/cost 采集,把只读投影升级为可追溯的真实审计与成本统计。
12. **管理端业务查询模型** 12. **管理端业务查询模型**
- 已吸收:管理端已有通用 sync record 接收、查询、payload 摘要和深链。 - 已吸收:管理端已有通用 sync record 接收、查询、payload 摘要和深链embedded adapter 已拆出 `/api/digital-employee/plans``/tasks``/runs``/events``/artifacts``/approvals``/plans/:planId` 只读业务查询接口,从 qimingclaw runtime、artifact、approval 和 canonical event 投影生成统一分页视图,本地业务视图投影已开始闭环
- 缺口:尚未拆出 `/api/digital-employee/plans/tasks/runs/events/artifacts/approvals` 业务视图;管理端仍主要从同步记录观察事实 - 缺口:尚未接入管理端真实业务表、复杂组合检索、跨设备维度过滤和业务视图 UI
- 建议:先从 sync record 聚合业务视图,不急于拆物理业务表。 - 建议:先稳定本地投影 API 和管理端消费模型,再决定是否拆物理业务表。
13. **日报实体与导出交付** 13. **日报实体与导出交付**
- 已吸收embedded 页面可基于当前 workday projection 导出本地文本日报。 - 已吸收embedded 页面可基于当前 workday projection 导出本地文本日报。