吸收数字员工业务查询视图
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 {
|
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
@@ -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>
|
||||||
|
|||||||
@@ -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 导出本地文本日报。
|
||||||
|
|||||||
Reference in New Issue
Block a user