吸收数字员工入口路由动作映射
This commit is contained in:
@@ -3185,6 +3185,16 @@ async function handleIngressRoute(
|
|||||||
entrypoint: body.entrypoint ?? null,
|
entrypoint: body.entrypoint ?? null,
|
||||||
actor: body.actor ?? 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 bridge = window.QimingClawBridge?.digital;
|
||||||
const recorded = bridge?.recordRouteDecision
|
const recorded = bridge?.recordRouteDecision
|
||||||
? await bridge.recordRouteDecision({
|
? await bridge.recordRouteDecision({
|
||||||
@@ -3213,6 +3223,7 @@ async function handleIngressRoute(
|
|||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
const routeDecision = recorded ?? decision;
|
const routeDecision = recorded ?? decision;
|
||||||
|
const resultIds = routeCommandResultIds(command);
|
||||||
return {
|
return {
|
||||||
accepted: true,
|
accepted: true,
|
||||||
envelope: {
|
envelope: {
|
||||||
@@ -3224,14 +3235,142 @@ async function handleIngressRoute(
|
|||||||
},
|
},
|
||||||
response_kind: 'route_decision',
|
response_kind: 'route_decision',
|
||||||
route_decision: routeDecision,
|
route_decision: routeDecision,
|
||||||
command: null,
|
command,
|
||||||
created_plan_id: null,
|
created_plan_id: resultIds.createdPlanId,
|
||||||
created_schedule_id: null,
|
created_schedule_id: resultIds.createdScheduleId,
|
||||||
created_task_ids: [],
|
created_task_ids: resultIds.createdTaskIds,
|
||||||
created_run_ids: [],
|
created_run_ids: resultIds.createdRunIds,
|
||||||
projection_name: null,
|
projection_name: null,
|
||||||
projection_payload: 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 dispatch = await dispatchPlan(targetPlanId, snapshot, { recordCommand: false });
|
||||||
const response = governanceAccepted(commandId, correlationId, '已映射为 qimingclaw 本地任务激活。', {
|
const response = governanceAccepted(commandId, correlationId, '已映射为 qimingclaw 本地任务激活。', {
|
||||||
plan_id: targetPlanId,
|
plan_id: targetPlanId,
|
||||||
|
schedule_id: body.command_kind === 'run_schedule_now' ? scheduleId : null,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
dispatched_tasks: dispatch.dispatched_tasks,
|
dispatched_tasks: dispatch.dispatched_tasks,
|
||||||
source: 'qimingclaw-adapter',
|
source: 'qimingclaw-adapter',
|
||||||
|
|||||||
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-DvAXzZFK.js"></script>
|
<script type="module" crossorigin src="./assets/index-2bfLPJ8G.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-BarRiSjT.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-BarRiSjT.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ describe("digital employee state service", () => {
|
|||||||
reasonCodes: ["ready_step_context"],
|
reasonCodes: ["ready_step_context"],
|
||||||
candidateSkills: ["qimingclaw-computer-control"],
|
candidateSkills: ["qimingclaw-computer-control"],
|
||||||
contextRefs: { plan_id: "plan-1", step_id: "step-ready" },
|
contextRefs: { plan_id: "plan-1", step_id: "step-ready" },
|
||||||
|
createdCommandId: "qimingclaw-digital-start_run-1",
|
||||||
createsTaskRun: true,
|
createsTaskRun: true,
|
||||||
entrypoint: "task_center",
|
entrypoint: "task_center",
|
||||||
actor: "operator",
|
actor: "operator",
|
||||||
@@ -221,6 +222,7 @@ describe("digital employee state service", () => {
|
|||||||
expect(first).toMatchObject({
|
expect(first).toMatchObject({
|
||||||
id: "route-decision:route-key-1",
|
id: "route-decision:route-key-1",
|
||||||
route_kind: "PlanExecution",
|
route_kind: "PlanExecution",
|
||||||
|
created_command_id: "qimingclaw-digital-start_run-1",
|
||||||
creates_task_run: true,
|
creates_task_run: true,
|
||||||
context_refs: { step_id: "step-ready" },
|
context_refs: { step_id: "step-ready" },
|
||||||
});
|
});
|
||||||
@@ -235,6 +237,7 @@ describe("digital employee state service", () => {
|
|||||||
id: first.id,
|
id: first.id,
|
||||||
route_kind: "PlanExecution",
|
route_kind: "PlanExecution",
|
||||||
intent_kind: "execute",
|
intent_kind: "execute",
|
||||||
|
created_command_id: "qimingclaw-digital-start_run-1",
|
||||||
},
|
},
|
||||||
envelope: { text: "开始执行这个步骤" },
|
envelope: { text: "开始执行这个步骤" },
|
||||||
});
|
});
|
||||||
@@ -245,7 +248,7 @@ describe("digital employee state service", () => {
|
|||||||
});
|
});
|
||||||
expect(listDigitalEmployeeRouteDecisions()).toEqual([
|
expect(listDigitalEmployeeRouteDecisions()).toEqual([
|
||||||
expect.objectContaining({ id: second.id, route_kind: "ProjectionQuery" }),
|
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" }),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -589,9 +589,9 @@ GET /api/digital-employee/approvals
|
|||||||
|
|
||||||
10. **入口路由 / 任务分流(路由决策事件闭环已开始)**
|
10. **入口路由 / 任务分流(路由决策事件闭环已开始)**
|
||||||
- 已吸收:管理端通过 `/computer/chat` 进入 qimingclaw 后会生成本地任务事实;embedded `/api/ingress/route` 和 `/api/route-decisions` 已由 qimingclaw adapter 接管。
|
- 已吸收:管理端通过 `/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、调度、查询和控制动作生成 route decision,并写入 `digital_events.kind = route_decision` 与现有 event outbox;路由时间线优先从 qimingclaw 本地事件读取;低风险 route decision 已开始映射为明确的本地 governance command,并把 `created_command_id` 回填到 route decision。
|
||||||
- 后续缺口:基于路由决策自动生成治理命令、ready PlanStep 安全执行触发、管理端路由时间线视图和风险策略下发仍待吸收。
|
- 后续缺口:ready PlanStep 自动调度守护、管理端路由时间线视图、风险策略下发和高风险动作审批策略仍待吸收。
|
||||||
- 建议:下一轮围绕 action endpoint,把低风险 route decision 映射为明确的 governance command。
|
- 建议:下一轮围绕 ready PlanStep 安全触发和管理端路由视图,把入口路由从单次 action 映射推进到可观测调度策略。
|
||||||
|
|
||||||
11. **诊断 / 成本 / 审计视图**
|
11. **诊断 / 成本 / 审计视图**
|
||||||
- 已吸收:同步失败诊断较完整,部分 sandbox / memory / service 日志在 qimingclaw 其他模块中存在。
|
- 已吸收:同步失败诊断较完整,部分 sandbox / memory / service 日志在 qimingclaw 其他模块中存在。
|
||||||
|
|||||||
Reference in New Issue
Block a user