吸收数字员工业务查询视图
This commit is contained in:
@@ -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 {
|
||||
return window.__QIMINGCLAW_EMBEDDED_DIGITAL__ === true;
|
||||
}
|
||||
@@ -593,6 +598,17 @@ export async function handleQimingclawDigitalApi<T>(
|
||||
if (method === 'POST' && pathname === '/api/digital-employee/workday/actions') {
|
||||
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') {
|
||||
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);
|
||||
}
|
||||
|
||||
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> {
|
||||
const payload = asRecord(event.payload) ?? {};
|
||||
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
@@ -16,7 +16,7 @@
|
||||
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
|
||||
}
|
||||
</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">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -599,9 +599,9 @@ GET /api/digital-employee/approvals
|
||||
- 建议:下一轮围绕 sandbox audit 与 token/cost 采集,把只读投影升级为可追溯的真实审计与成本统计。
|
||||
|
||||
12. **管理端业务查询模型**
|
||||
- 已吸收:管理端已有通用 sync record 接收、查询、payload 摘要和深链。
|
||||
- 缺口:尚未拆出 `/api/digital-employee/plans/tasks/runs/events/artifacts/approvals` 业务视图;管理端仍主要从同步记录观察事实。
|
||||
- 建议:先从 sync record 聚合业务视图,不急于拆物理业务表。
|
||||
- 已吸收:管理端已有通用 sync record 接收、查询、payload 摘要和深链;embedded adapter 已拆出 `/api/digital-employee/plans`、`/tasks`、`/runs`、`/events`、`/artifacts`、`/approvals` 与 `/plans/:planId` 只读业务查询接口,从 qimingclaw runtime、artifact、approval 和 canonical event 投影生成统一分页视图,本地业务视图投影已开始闭环。
|
||||
- 缺口:尚未接入管理端真实业务表、复杂组合检索、跨设备维度过滤和业务视图 UI。
|
||||
- 建议:先稳定本地投影 API 和管理端消费模型,再决定是否拆物理业务表。
|
||||
|
||||
13. **日报实体与导出交付**
|
||||
- 已吸收:embedded 页面可基于当前 workday projection 导出本地文本日报。
|
||||
|
||||
Reference in New Issue
Block a user