吸收数字员工入口路由观测
This commit is contained in:
@@ -2027,7 +2027,7 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto minmax(96px, 0.58fr) auto minmax(0, 1fr);
|
grid-template-rows: auto auto minmax(96px, 0.58fr) auto minmax(0, 1fr);
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.de-execution-status-strip {
|
.de-execution-status-strip {
|
||||||
@@ -2044,6 +2044,21 @@
|
|||||||
}
|
}
|
||||||
.de-execution-status-strip em { color: #5b7590; font-size: 11px; font-style: normal; white-space: nowrap; }
|
.de-execution-status-strip em { color: #5b7590; font-size: 11px; font-style: normal; white-space: nowrap; }
|
||||||
.de-execution-status-strip strong { color: #12344f; font-size: 18px; line-height: 1; }
|
.de-execution-status-strip strong { color: #12344f; font-size: 18px; line-height: 1; }
|
||||||
|
.de-route-summary-strip {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(54px, 0.36fr)) minmax(0, 1fr);
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 7px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(247,251,255,0.88);
|
||||||
|
border: 1px solid rgba(79,172,254,0.12);
|
||||||
|
}
|
||||||
|
.de-route-summary-strip span { min-width: 0; display: grid; gap: 2px; }
|
||||||
|
.de-route-summary-strip em { color: #5b7590; font-size: 10px; font-style: normal; white-space: nowrap; }
|
||||||
|
.de-route-summary-strip strong { color: #12344f; font-size: 14px; line-height: 1; }
|
||||||
|
.de-route-summary-strip .is-warning strong { color: #a33b3b; }
|
||||||
|
.de-route-summary-strip small { min-width: 0; color: #41627f; font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.de-task-summary-scroll {
|
.de-task-summary-scroll {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -3121,7 +3136,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.de-workday-execution-body {
|
.de-workday-execution-body {
|
||||||
grid-template-rows: auto auto auto auto;
|
grid-template-rows: auto auto auto auto auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.de-task-summary-scroll,
|
.de-task-summary-scroll,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import type {
|
|||||||
DigitalEmployeeDuty,
|
DigitalEmployeeDuty,
|
||||||
DigitalEmployeeManagedService,
|
DigitalEmployeeManagedService,
|
||||||
DigitalEmployeePlanAgentState,
|
DigitalEmployeePlanAgentState,
|
||||||
|
DigitalEmployeeRouteSummary,
|
||||||
DigitalEmployeeRunDetail,
|
DigitalEmployeeRunDetail,
|
||||||
DigitalEmployeeTaskAgentState,
|
DigitalEmployeeTaskAgentState,
|
||||||
DigitalEmployeeTaskExecutionSummary,
|
DigitalEmployeeTaskExecutionSummary,
|
||||||
@@ -671,7 +672,7 @@ const PLAN_SCHEDULES: Record<string, { expression: string; nextRun: string; sche
|
|||||||
|
|
||||||
const BUSINESS_VIEW_SOURCE = 'qimingclaw-runtime-projection';
|
const BUSINESS_VIEW_SOURCE = 'qimingclaw-runtime-projection';
|
||||||
|
|
||||||
type BusinessViewKind = 'plans' | 'tasks' | 'runs' | 'events' | 'artifacts' | 'approvals';
|
type BusinessViewKind = 'plans' | 'tasks' | 'runs' | 'events' | 'artifacts' | 'approvals' | 'route-decisions';
|
||||||
type BusinessViewRow = Record<string, unknown>;
|
type BusinessViewRow = Record<string, unknown>;
|
||||||
|
|
||||||
export function isQimingclawDigitalApiEnabled(): boolean {
|
export function isQimingclawDigitalApiEnabled(): boolean {
|
||||||
@@ -755,7 +756,7 @@ export async function handleQimingclawDigitalApi<T>(
|
|||||||
const approvalId = decodeURIComponent(approvalActionMatch[1] || '');
|
const approvalId = decodeURIComponent(approvalActionMatch[1] || '');
|
||||||
return await handleApprovalAction(approvalId, parseOptionalJsonBody(options.body), await readQimingclawSnapshot()) as T;
|
return await handleApprovalAction(approvalId, parseOptionalJsonBody(options.body), await readQimingclawSnapshot()) as T;
|
||||||
}
|
}
|
||||||
const businessViewMatch = pathname.match(/^\/api\/digital-employee\/(plans|tasks|runs|events|artifacts|approvals)$/);
|
const businessViewMatch = pathname.match(/^\/api\/digital-employee\/(plans|tasks|runs|events|artifacts|approvals|route-decisions)$/);
|
||||||
if (method === 'GET' && businessViewMatch) {
|
if (method === 'GET' && businessViewMatch) {
|
||||||
return buildBusinessViewResponse(businessViewMatch[1] as BusinessViewKind, await readQimingclawSnapshot(), url.searchParams) as T;
|
return buildBusinessViewResponse(businessViewMatch[1] as BusinessViewKind, await readQimingclawSnapshot(), url.searchParams) as T;
|
||||||
}
|
}
|
||||||
@@ -2372,14 +2373,109 @@ function buildBusinessViewResponse(kind: BusinessViewKind, snapshot: QimingclawS
|
|||||||
events: businessEventRows(snapshot),
|
events: businessEventRows(snapshot),
|
||||||
artifacts: businessArtifactRows(snapshot),
|
artifacts: businessArtifactRows(snapshot),
|
||||||
approvals: businessApprovalRows(snapshot),
|
approvals: businessApprovalRows(snapshot),
|
||||||
|
'route-decisions': routeDecisionRows(snapshot),
|
||||||
};
|
};
|
||||||
const rows = filterBusinessRows(rowsByKind[kind], searchParams);
|
const rows = kind === 'route-decisions'
|
||||||
|
? filterRouteDecisionRows(rowsByKind[kind], searchParams)
|
||||||
|
: filterBusinessRows(rowsByKind[kind], searchParams);
|
||||||
return {
|
return {
|
||||||
...paginateBusinessRows(rows, searchParams),
|
...paginateBusinessRows(rows, searchParams),
|
||||||
source: BUSINESS_VIEW_SOURCE,
|
source: BUSINESS_VIEW_SOURCE,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 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[] {
|
function businessPlanRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
|
||||||
const runtimeById = new Map(runtimePlans(snapshot).map((plan) => [plan.id, plan]));
|
const runtimeById = new Map(runtimePlans(snapshot).map((plan) => [plan.id, plan]));
|
||||||
const tasks = buildTasks(null, snapshot);
|
const tasks = buildTasks(null, snapshot);
|
||||||
@@ -3242,6 +3338,7 @@ function buildNotifications(snapshot: QimingclawSnapshot | null, state: AdapterS
|
|||||||
...syncFailureNotifications(snapshot),
|
...syncFailureNotifications(snapshot),
|
||||||
...managedServiceNotifications(snapshot),
|
...managedServiceNotifications(snapshot),
|
||||||
...planStepDispatchNotifications(snapshot),
|
...planStepDispatchNotifications(snapshot),
|
||||||
|
...routeDecisionNotifications(snapshot),
|
||||||
...taskNotifications(snapshot),
|
...taskNotifications(snapshot),
|
||||||
]);
|
]);
|
||||||
const overlays = state.notificationStateById ?? {};
|
const overlays = state.notificationStateById ?? {};
|
||||||
@@ -3376,6 +3473,31 @@ function planStepDispatchNotifications(snapshot: QimingclawSnapshot | null): Use
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 applyNotificationOverlay(notification: UserNotification, overlay?: NotificationState): UserNotification {
|
function applyNotificationOverlay(notification: UserNotification, overlay?: NotificationState): UserNotification {
|
||||||
if (!overlay) return notification;
|
if (!overlay) return notification;
|
||||||
return {
|
return {
|
||||||
@@ -4066,6 +4188,7 @@ function buildWorkday(date: string, snapshot: QimingclawSnapshot | null): Digita
|
|||||||
})),
|
})),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
route_summary: buildRouteDecisionSummary(snapshot),
|
||||||
daily_report: buildDailyReportProjection({ date, state, generatedAt, selectedCount, events, executionSummaries, runDetails, artifactSources, successCount, failureCount, runningCount, snapshot }),
|
daily_report: buildDailyReportProjection({ date, state, generatedAt, selectedCount, events, executionSummaries, runDetails, artifactSources, successCount, failureCount, runningCount, snapshot }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1080,6 +1080,8 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
|||||||
const managedServiceIssues = managedServices.filter((service) => service.requires_human_action || service.status === 'degraded');
|
const managedServiceIssues = managedServices.filter((service) => service.requires_human_action || service.status === 'degraded');
|
||||||
const taskAgents = isLiveProjection ? (workdayProjection?.task_agents ?? []) : [];
|
const taskAgents = isLiveProjection ? (workdayProjection?.task_agents ?? []) : [];
|
||||||
const actionableTaskAgents = taskAgents.filter((agent) => agent.can_retry || agent.can_skip || agent.can_cancel);
|
const actionableTaskAgents = taskAgents.filter((agent) => agent.can_retry || agent.can_skip || agent.can_cancel);
|
||||||
|
const routeSummary = isLiveProjection ? workdayProjection?.route_summary : null;
|
||||||
|
const latestRoute = routeSummary?.latest ?? null;
|
||||||
const dailyReport = isLiveProjection ? workdayProjection?.daily_report : undefined;
|
const dailyReport = isLiveProjection ? workdayProjection?.daily_report : undefined;
|
||||||
const runDetails = isLiveProjection
|
const runDetails = isLiveProjection
|
||||||
? (workdayProjection?.run_details?.length ? workdayProjection.run_details : allEvents.map(runDetailFromEvent))
|
? (workdayProjection?.run_details?.length ? workdayProjection.run_details : allEvents.map(runDetailFromEvent))
|
||||||
@@ -2004,6 +2006,15 @@ export default function CommandDeck({ onNavigate }: { onNavigate: (target: Digit
|
|||||||
<span><em>异常</em><strong>{failedEvents.length}</strong></span>
|
<span><em>异常</em><strong>{failedEvents.length}</strong></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{routeSummary && routeSummary.total > 0 && (
|
||||||
|
<div className="de-route-summary-strip">
|
||||||
|
<span><em>入口路由</em><strong>{routeSummary.total}</strong></span>
|
||||||
|
<span><em>已映射</em><strong>{routeSummary.mapped_count}</strong></span>
|
||||||
|
<span className={routeSummary.attention_count > 0 ? 'is-warning' : ''}><em>需关注</em><strong>{routeSummary.attention_count}</strong></span>
|
||||||
|
<small>{latestRoute ? `${latestRoute.route_kind} / ${latestRoute.intent_kind} · ${latestRoute.status_label}` : '暂无路由决策'}</small>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="de-task-summary-scroll">
|
<div className="de-task-summary-scroll">
|
||||||
<div className="de-task-summary-grid" aria-label="任务执行汇总">
|
<div className="de-task-summary-grid" aria-label="任务执行汇总">
|
||||||
{taskSummaryCards.length === 0 ? (
|
{taskSummaryCards.length === 0 ? (
|
||||||
|
|||||||
@@ -835,6 +835,21 @@ export interface DigitalEmployeeDailyReport {
|
|||||||
pdf_url?: string | null;
|
pdf_url?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DigitalEmployeeRouteSummary {
|
||||||
|
total: number;
|
||||||
|
mapped_count: number;
|
||||||
|
attention_count: number;
|
||||||
|
latest?: {
|
||||||
|
id: string;
|
||||||
|
route_kind: string;
|
||||||
|
intent_kind: string;
|
||||||
|
status_label: string;
|
||||||
|
attention_level: string;
|
||||||
|
created_command_id?: string | null;
|
||||||
|
recorded_at: string;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DigitalEmployeeWorkdayProjection {
|
export interface DigitalEmployeeWorkdayProjection {
|
||||||
source: DigitalEmployeeProjectionSource;
|
source: DigitalEmployeeProjectionSource;
|
||||||
demo_reason?: string | null;
|
demo_reason?: string | null;
|
||||||
@@ -851,6 +866,7 @@ export interface DigitalEmployeeWorkdayProjection {
|
|||||||
run_details?: DigitalEmployeeRunDetail[];
|
run_details?: DigitalEmployeeRunDetail[];
|
||||||
decisions: DigitalEmployeeDecision[];
|
decisions: DigitalEmployeeDecision[];
|
||||||
delivery: DigitalEmployeeDelivery;
|
delivery: DigitalEmployeeDelivery;
|
||||||
|
route_summary?: DigitalEmployeeRouteSummary;
|
||||||
daily_report?: DigitalEmployeeDailyReport;
|
daily_report?: DigitalEmployeeDailyReport;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -16,8 +16,8 @@
|
|||||||
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-Cb1LtojZ.js"></script>
|
<script type="module" crossorigin src="./assets/index-YhUdeF7L.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-DKoke9Cq.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-DSHN6Btj.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -592,6 +592,19 @@ describe("digital employee state service", () => {
|
|||||||
contextRefs: { plan_id: "plan-1" },
|
contextRefs: { plan_id: "plan-1" },
|
||||||
recordedAt: "2026-06-07T08:02:00.000Z",
|
recordedAt: "2026-06-07T08:02:00.000Z",
|
||||||
});
|
});
|
||||||
|
const attention = recordDigitalEmployeeRouteDecision({
|
||||||
|
idempotencyKey: "route-key-attention",
|
||||||
|
correlationId: "corr-attention",
|
||||||
|
routeKind: "PlanExecution",
|
||||||
|
intentKind: "execute",
|
||||||
|
reasonCodes: ["candidate_skills_disabled", "route_action_missing_context"],
|
||||||
|
candidateSkills: ["qimingclaw-computer-control"],
|
||||||
|
policyResult: "accepted",
|
||||||
|
entrypoint: "webhook",
|
||||||
|
actor: "management-api",
|
||||||
|
envelope: { text: "执行但缺少 task_id" },
|
||||||
|
recordedAt: "2026-06-07T08:03:00.000Z",
|
||||||
|
});
|
||||||
|
|
||||||
expect(first).toMatchObject({
|
expect(first).toMatchObject({
|
||||||
id: "route-decision:route-key-1",
|
id: "route-decision:route-key-1",
|
||||||
@@ -615,12 +628,23 @@ describe("digital employee state service", () => {
|
|||||||
},
|
},
|
||||||
envelope: { text: "开始执行这个步骤" },
|
envelope: { text: "开始执行这个步骤" },
|
||||||
});
|
});
|
||||||
|
expect(JSON.parse(mockState.db?.events.get(attention.id)?.payload ?? "{}")).toMatchObject({
|
||||||
|
routeDecision: {
|
||||||
|
id: attention.id,
|
||||||
|
reason_codes: ["candidate_skills_disabled", "route_action_missing_context"],
|
||||||
|
candidate_skills: ["qimingclaw-computer-control"],
|
||||||
|
entrypoint: "webhook",
|
||||||
|
actor: "management-api",
|
||||||
|
},
|
||||||
|
envelope: { text: "执行但缺少 task_id" },
|
||||||
|
});
|
||||||
expect(mockState.db?.outbox.get(`event:insert:${first.id}`)).toMatchObject({
|
expect(mockState.db?.outbox.get(`event:insert:${first.id}`)).toMatchObject({
|
||||||
entity_type: "event",
|
entity_type: "event",
|
||||||
operation: "insert",
|
operation: "insert",
|
||||||
status: "pending",
|
status: "pending",
|
||||||
});
|
});
|
||||||
expect(listDigitalEmployeeRouteDecisions()).toEqual([
|
expect(listDigitalEmployeeRouteDecisions()).toEqual([
|
||||||
|
expect.objectContaining({ id: attention.id, reason_codes: ["candidate_skills_disabled", "route_action_missing_context"] }),
|
||||||
expect.objectContaining({ id: second.id, route_kind: "ProjectionQuery" }),
|
expect.objectContaining({ id: second.id, route_kind: "ProjectionQuery" }),
|
||||||
expect.objectContaining({ id: first.id, route_kind: "PlanExecution", created_command_id: "qimingclaw-digital-start_run-1" }),
|
expect.objectContaining({ id: first.id, route_kind: "PlanExecution", created_command_id: "qimingclaw-digital-start_run-1" }),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1630,8 +1630,8 @@ function routeDecisionFromEventPayload(payload: unknown): DigitalEmployeeRouteDe
|
|||||||
id,
|
id,
|
||||||
route_kind: stringValue(decision.route_kind ?? decision.routeKind) || "ChatOnly",
|
route_kind: stringValue(decision.route_kind ?? decision.routeKind) || "ChatOnly",
|
||||||
intent_kind: stringValue(decision.intent_kind ?? decision.intentKind) || "chat",
|
intent_kind: stringValue(decision.intent_kind ?? decision.intentKind) || "chat",
|
||||||
reason_codes: parseStringArray(decision.reason_codes ?? decision.reasonCodes),
|
reason_codes: parseRouteDecisionStringArray(decision.reason_codes ?? decision.reasonCodes),
|
||||||
candidate_skills: parseStringArray(decision.candidate_skills ?? decision.candidateSkills),
|
candidate_skills: parseRouteDecisionStringArray(decision.candidate_skills ?? decision.candidateSkills),
|
||||||
context_refs: objectRecord(decision.context_refs ?? decision.contextRefs) ?? {},
|
context_refs: objectRecord(decision.context_refs ?? decision.contextRefs) ?? {},
|
||||||
created_command_id: stringValue(decision.created_command_id ?? decision.createdCommandId) || null,
|
created_command_id: stringValue(decision.created_command_id ?? decision.createdCommandId) || null,
|
||||||
correlation_id: stringValue(decision.correlation_id ?? decision.correlationId) || id,
|
correlation_id: stringValue(decision.correlation_id ?? decision.correlationId) || id,
|
||||||
@@ -1645,6 +1645,11 @@ function routeDecisionFromEventPayload(payload: unknown): DigitalEmployeeRouteDe
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseRouteDecisionStringArray(value: unknown): string[] {
|
||||||
|
if (Array.isArray(value)) return normalizeSkillIds(value);
|
||||||
|
return parseStringArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
function routeDecisionMessage(decision: DigitalEmployeeRouteDecisionRecord): string {
|
function routeDecisionMessage(decision: DigitalEmployeeRouteDecisionRecord): string {
|
||||||
return `入口路由决策:${decision.route_kind} / ${decision.intent_kind}`;
|
return `入口路由决策:${decision.route_kind} / ${decision.intent_kind}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -587,11 +587,11 @@ GET /api/digital-employee/approvals
|
|||||||
- 缺口:仍缺少系统级桌面通知、管理端通知视图、通知订阅策略、跨端未读同步和任务输入自动转化。
|
- 缺口:仍缺少系统级桌面通知、管理端通知视图、通知订阅策略、跨端未读同步和任务输入自动转化。
|
||||||
- 建议:下一轮围绕通知订阅策略和管理端通知视图,把通知从本地 Inbox 投影推进到跨端协同入口。
|
- 建议:下一轮围绕通知订阅策略和管理端通知视图,把通知从本地 Inbox 投影推进到跨端协同入口。
|
||||||
|
|
||||||
10. **入口路由 / 任务分流(路由决策事件闭环已开始)**
|
10. **入口路由 / 任务分流(路由业务视图与通知联动已开始)**
|
||||||
- 已吸收:管理端通过 `/computer/chat` 进入 qimingclaw 后会生成本地任务事实;embedded `/api/ingress/route` 和 `/api/route-decisions` 已由 qimingclaw adapter 接管。
|
- 已吸收:管理端通过 `/computer/chat` 进入 qimingclaw 后会生成本地任务事实;embedded `/api/ingress/route`、`/api/route-decisions` 和 `/api/digital-employee/route-decisions` 已由 qimingclaw adapter 接管。
|
||||||
- 当前能力:入口请求会按上下文、ready PlanStep、调度、查询和控制动作生成 route decision,并写入 `digital_events.kind = route_decision` 与现有 event outbox;路由时间线优先从 qimingclaw 本地事件读取;低风险 route decision 已开始映射为明确的本地 governance command,并把 `created_command_id` 回填到 route decision;scheduler check 已接入 ready PlanStep 安全派发,首页立即执行后会触发一次派发 sweep 并展示派发结果。
|
- 当前能力:入口请求会按上下文、ready PlanStep、调度、查询和控制动作生成 route decision,并写入 `digital_events.kind = route_decision` 与现有 event outbox;路由时间线优先从 qimingclaw 本地事件读取;低风险 route decision 已开始映射为明确的本地 governance command,并把 `created_command_id` 回填到 route decision;scheduler check 已接入 ready PlanStep 安全派发,首页立即执行后会触发一次派发 sweep 并展示派发结果;路由决策本地业务视图、首页摘要和通知联动已开始闭环。
|
||||||
- 后续缺口:管理端路由时间线视图、派发策略下发、风险策略下发和高风险动作审批策略仍待吸收。
|
- 后续缺口:管理端正式路由 UI、派发策略下发、风险策略下发和高风险动作审批策略仍待吸收。
|
||||||
- 建议:下一轮围绕管理端路由视图和策略下发,把入口路由从本地派发推进到跨端可观测调度策略。
|
- 建议:下一轮围绕管理端正式路由 UI 和策略下发,把入口路由从本地可观测推进到跨端治理策略。
|
||||||
|
|
||||||
11. **诊断 / 成本 / 审计视图**
|
11. **诊断 / 成本 / 审计视图**
|
||||||
- 已吸收:同步失败诊断较完整,部分 sandbox / memory / service 日志在 qimingclaw 其他模块中存在;embedded adapter 已接管 `/api/doctor`、`/api/cost`、`/api/audit`,从 snapshot、runtime records、sync status、managed services 和 artifact delivery facts 生成只读诊断、估算成本和审计投影,只读投影已开始闭环。
|
- 已吸收:同步失败诊断较完整,部分 sandbox / memory / service 日志在 qimingclaw 其他模块中存在;embedded adapter 已接管 `/api/doctor`、`/api/cost`、`/api/audit`,从 snapshot、runtime records、sync status、managed services 和 artifact delivery facts 生成只读诊断、估算成本和审计投影,只读投影已开始闭环。
|
||||||
|
|||||||
Reference in New Issue
Block a user