吸收数字员工入口路由动作映射
This commit is contained in:
@@ -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',
|
||||
|
||||
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-DvAXzZFK.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-2bfLPJ8G.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-BarRiSjT.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -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" }),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -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 其他模块中存在。
|
||||
|
||||
Reference in New Issue
Block a user