吸收数字员工入口路由动作映射

This commit is contained in:
baiyanyun
2026-06-09 19:05:15 +08:00
parent 23c3577ef4
commit 755a8f041d
5 changed files with 188 additions and 45 deletions

View File

@@ -3185,6 +3185,16 @@ async function handleIngressRoute(
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({
@@ -3213,6 +3223,7 @@ async function handleIngressRoute(
})
: null;
const routeDecision = recorded ?? decision;
const resultIds = routeCommandResultIds(command);
return {
accepted: true,
envelope: {
@@ -3224,14 +3235,142 @@ async function handleIngressRoute(
},
response_kind: 'route_decision',
route_decision: routeDecision,
command: null,
created_plan_id: null,
created_schedule_id: null,
created_task_ids: [],
created_run_ids: [],
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: `已记录入口路由:${routeDecision.route_kind} / ${routeDecision.intent_kind}`,
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]),
};
}
@@ -3398,6 +3537,7 @@ async function handleGovernanceCommand(
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',

View File

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

View File

@@ -201,6 +201,7 @@ describe("digital employee state service", () => {
reasonCodes: ["ready_step_context"],
candidateSkills: ["qimingclaw-computer-control"],
contextRefs: { plan_id: "plan-1", step_id: "step-ready" },
createdCommandId: "qimingclaw-digital-start_run-1",
createsTaskRun: true,
entrypoint: "task_center",
actor: "operator",
@@ -221,6 +222,7 @@ describe("digital employee state service", () => {
expect(first).toMatchObject({
id: "route-decision:route-key-1",
route_kind: "PlanExecution",
created_command_id: "qimingclaw-digital-start_run-1",
creates_task_run: true,
context_refs: { step_id: "step-ready" },
});
@@ -235,6 +237,7 @@ describe("digital employee state service", () => {
id: first.id,
route_kind: "PlanExecution",
intent_kind: "execute",
created_command_id: "qimingclaw-digital-start_run-1",
},
envelope: { text: "开始执行这个步骤" },
});
@@ -245,7 +248,7 @@ describe("digital employee state service", () => {
});
expect(listDigitalEmployeeRouteDecisions()).toEqual([
expect.objectContaining({ id: second.id, route_kind: "ProjectionQuery" }),
expect.objectContaining({ id: first.id, route_kind: "PlanExecution" }),
expect.objectContaining({ id: first.id, route_kind: "PlanExecution", created_command_id: "qimingclaw-digital-start_run-1" }),
]);
});

View File

@@ -589,9 +589,9 @@ GET /api/digital-employee/approvals
10. **入口路由 / 任务分流(路由决策事件闭环已开始)**
- 已吸收:管理端通过 `/computer/chat` 进入 qimingclaw 后会生成本地任务事实embedded `/api/ingress/route``/api/route-decisions` 已由 qimingclaw adapter 接管。
- 当前能力入口请求会按上下文、ready PlanStep、调度、查询和控制动作生成 route decision并写入 `digital_events.kind = route_decision` 与现有 event outbox路由时间线优先从 qimingclaw 本地事件读取。
- 后续缺口:基于路由决策自动生成治理命令、ready PlanStep 安全执行触发、管理端路由时间线视图风险策略下发仍待吸收。
- 建议:下一轮围绕 action endpoint把低风险 route decision 映射为明确的 governance command
- 当前能力入口请求会按上下文、ready PlanStep、调度、查询和控制动作生成 route decision并写入 `digital_events.kind = route_decision` 与现有 event outbox路由时间线优先从 qimingclaw 本地事件读取;低风险 route decision 已开始映射为明确的本地 governance command并把 `created_command_id` 回填到 route decision
- 后续缺口ready PlanStep 自动调度守护、管理端路由时间线视图风险策略下发和高风险动作审批策略仍待吸收。
- 建议:下一轮围绕 ready PlanStep 安全触发和管理端路由视图,把入口路由从单次 action 映射推进到可观测调度策略
11. **诊断 / 成本 / 审计视图**
- 已吸收:同步失败诊断较完整,部分 sandbox / memory / service 日志在 qimingclaw 其他模块中存在。