吸收数字员工治理动作入口

This commit is contained in:
baiyanyun
2026-06-09 15:54:49 +08:00
parent 702ea05d4d
commit 9ad7881378
6 changed files with 348 additions and 142 deletions

View File

@@ -658,6 +658,36 @@ export async function handleQimingclawDigitalApi<T>(
if (method === 'POST' && pathname.startsWith('/api/debug/plan/') && pathname.endsWith('/dispatch')) {
return await dispatchPlan(decodeURIComponent(pathname.split('/')[4] || ''), await readQimingclawSnapshot()) as T;
}
const debugPlanActionMatch = pathname.match(/^\/api\/debug\/plan\/([^/]+)\/([^/]+)$/);
if (method === 'POST' && debugPlanActionMatch) {
return await handlePlanAction(
decodeURIComponent(debugPlanActionMatch[1] || ''),
decodeURIComponent(debugPlanActionMatch[2] || ''),
await readQimingclawSnapshot(),
parseOptionalJsonBody(options.body),
) as T;
}
const planSubmitMatch = pathname.match(/^\/api\/plan\/([^/]+)\/submit$/);
if (method === 'POST' && planSubmitMatch) {
return await handlePlanAction(
decodeURIComponent(planSubmitMatch[1] || ''),
'submit',
await readQimingclawSnapshot(),
parseOptionalJsonBody(options.body),
) as T;
}
if (method === 'GET' && pathname.startsWith('/api/plan/') && pathname.endsWith('/revisions')) {
return buildPlanRevisions(decodeURIComponent(pathname.split('/')[3] || ''), await readQimingclawSnapshot()) as T;
}
const taskActionMatch = pathname.match(/^\/api\/task\/([^/]+)\/([^/]+)$/);
if (method === 'POST' && taskActionMatch) {
return await handleTaskAction(
decodeURIComponent(taskActionMatch[1] || ''),
decodeURIComponent(taskActionMatch[2] || ''),
await readQimingclawSnapshot(),
parseOptionalJsonBody(options.body),
) as T;
}
if (method === 'GET' && pathname.startsWith('/api/plan/') && pathname.endsWith('/messages')) {
return { messages: buildPlanMessages(decodeURIComponent(pathname.split('/')[3] || ''), await readQimingclawSnapshot()) } as T;
}
@@ -981,6 +1011,10 @@ function parseJsonBody<T>(body: BodyInit | null | undefined): T {
return JSON.parse(body) as T;
}
function parseOptionalJsonBody(body: BodyInit | null | undefined): Record<string, unknown> {
return parseJsonBody<Record<string, unknown>>(body);
}
function todayInputDate(): string {
const date = new Date();
return [
@@ -2615,6 +2649,142 @@ function acpResponseForDecision(decision: string): 'once' | 'always' | 'reject'
return 'once';
}
async function handlePlanAction(
planId: string,
action: string,
snapshot: QimingclawSnapshot | null,
payload: Record<string, unknown> = {},
): Promise<GovernanceCommandResponse> {
const commandKind = planActionCommandKind(action);
const correlationId = `plan-action-${slugifyIdPart(planId)}-${slugifyIdPart(action)}-${Date.now()}`;
if (!commandKind) {
return {
command_id: `qimingclaw-digital-plan-action-${Date.now()}`,
accepted: false,
correlation_id: correlationId,
message: `暂不支持计划动作:${action}`,
error: 'unsupported_command',
};
}
return handleGovernanceCommand({
command_kind: commandKind,
context_refs: { plan_id: planId },
payload: {
...payload,
plan_id: planId,
requested_action: `plan_${action}`,
action_endpoint: `/api/debug/plan/${planId}/${action}`,
},
correlation_id: correlationId,
}, snapshot);
}
async function handleTaskAction(
taskId: string,
action: string,
snapshot: QimingclawSnapshot | null,
payload: Record<string, unknown> = {},
): Promise<GovernanceCommandResponse> {
const commandKind = taskActionCommandKind(action);
const task = runtimeTasks(snapshot).find((candidate) => candidate.id === taskId);
const latestRun = latestRunForTask(runtimeRuns(snapshot), taskId);
const planId = task?.planId ?? (stringValue(payload.plan_id) || undefined);
const runId = latestRun?.id ?? (stringValue(payload.run_id) || undefined);
const stepId = task ? taskStepId(task) : (stringValue(payload.step_id) || undefined);
const correlationId = `task-action-${slugifyIdPart(taskId)}-${slugifyIdPart(action)}-${Date.now()}`;
if (!commandKind) {
return {
command_id: `qimingclaw-digital-task-action-${Date.now()}`,
accepted: false,
correlation_id: correlationId,
message: `暂不支持任务动作:${action}`,
error: 'unsupported_command',
};
}
return handleGovernanceCommand({
command_kind: commandKind,
context_refs: {
plan_id: planId,
task_id: taskId,
run_id: runId,
step_id: stepId,
},
payload: {
...payload,
task_id: taskId,
plan_id: planId ?? payload.plan_id,
run_id: runId ?? payload.run_id,
step_id: stepId ?? payload.step_id,
requested_action: `task_${action}`,
action_endpoint: `/api/task/${taskId}/${action}`,
},
correlation_id: correlationId,
}, snapshot);
}
function planActionCommandKind(action: string): GovernanceCommandRequest['command_kind'] | null {
switch (action.toLowerCase()) {
case 'submit':
return 'submit_plan';
case 'approve':
return 'approve_plan';
case 'activate':
return 'activate_plan';
case 'pause':
return 'pause_plan';
case 'resume':
return 'resume_plan';
case 'cancel':
return 'cancel_plan';
case 'amend':
return 'amend_plan';
default:
return null;
}
}
function taskActionCommandKind(action: string): GovernanceCommandRequest['command_kind'] | null {
switch (action.toLowerCase()) {
case 'retry':
return 'retry_task';
case 'skip':
return 'skip_task';
case 'pause':
return 'pause_task';
case 'resume':
return 'resume_task';
case 'cancel':
return 'cancel_task';
case 'start':
return 'start_run';
default:
return null;
}
}
function buildPlanRevisions(planId: string, snapshot: QimingclawSnapshot | null): { plan_id: string; revisions: Array<Record<string, unknown>> } {
const revisions = runtimeEvents(snapshot)
.filter((event) => event.planId === planId && event.kind.startsWith('governance_'))
.sort((left, right) => right.occurredAt.localeCompare(left.occurredAt) || right.createdAt.localeCompare(left.createdAt))
.map((event) => {
const payload = asRecord(event.payload) ?? {};
const result = asRecord(payload.result) ?? {};
const commandKind = stringValue(payload.commandKind) || event.kind.replace(/^governance_/, '');
return {
revision_id: event.id,
plan_id: planId,
event_id: event.id,
command_kind: commandKind,
action: commandKind.replace(/_plan$/, ''),
status: stringValue(result.status) || stringValue(payload.status) || 'recorded',
message: event.message,
created_at: event.occurredAt,
payload,
};
});
return { plan_id: planId, revisions };
}
async function readRouteDecisions(limit = 80): Promise<RouteDecisionTimelineResponse> {
const bridge = window.QimingClawBridge?.digital;
if (!bridge?.listRouteDecisions) {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -440,6 +440,41 @@ describe("digital employee state service", () => {
});
});
it("records plan action governance commands as local run facts", async () => {
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
recordDigitalEmployeeGovernanceCommand({
commandKind: "pause_plan",
commandId: "command-pause-plan",
planId: "plan-action",
accepted: true,
message: "暂停计划执行",
payload: { plan_id: "plan-action", requested_action: "plan_pause" },
result: { plan_id: "plan-action", status: "paused" },
});
expect(mockState.db?.plans.get("plan-action")).toMatchObject({
status: "paused",
sync_status: "pending",
});
expect(mockState.db?.tasks.get("governance-task-command-pause-plan")).toMatchObject({
plan_id: "plan-action",
status: "paused",
assigned_skill_id: "qimingclaw-governance-command",
});
expect(mockState.db?.runs.get("governance-run-command-pause-plan")).toMatchObject({
plan_id: "plan-action",
status: "paused",
finished_at: "2026-06-07T08:00:00.000Z",
});
expect(mockState.db?.events.get("governance-run-command-pause-plan:pause_plan:command-pause-plan")).toMatchObject({
kind: "governance_pause_plan",
message: "暂停计划执行",
});
expect(mockState.db?.outbox.get("plan:upsert:plan-action")).toMatchObject({ entity_type: "plan" });
expect(mockState.db?.outbox.get("event:insert:governance-run-command-pause-plan:pause_plan:command-pause-plan")).toMatchObject({ entity_type: "event" });
});
it("records schedule lifecycle commands and manual runs", async () => {
const {
readDigitalEmployeeRuntimeRecords,

View File

@@ -557,9 +557,10 @@ GET /api/digital-employee/approvals
- 建议:下一轮围绕管理端视图和记忆治理,把长期记忆从本地事实继续扩展到人工可审计、可归档、可合并的运营闭环。
4. **治理命令完整生命周期**
- 已吸收:`create_plan``submit_plan``approve_plan``activate_plan` 等基础命令可记录本地运行事实;任务级 `retry_task``skip_task``pause_task``resume_task``provide_task_input``cancel_task``start_run``cancel_run` 已具备本地事实闭环。
- 缺口:`escalate`、计划 revision、任务 action endpoint、治理审计链和对真实 ACP 执行流的硬中断仍不完整
- 建议:下一轮围绕治理审计和依赖调度补齐更细的 action endpoint
- 已吸收:`create_plan``submit_plan``approve_plan``activate_plan` 等基础命令可记录本地运行事实;任务级 `retry_task``skip_task``pause_task``resume_task``provide_task_input``cancel_task``start_run``cancel_run` 已具备本地事实闭环embedded plan / task action endpoint 已开始映射为正式 governance command
- 当前能力:`/api/plan/:id/submit``/api/debug/plan/:id/:action``/api/task/:id/:action` 会进入 qimingclaw 本地治理记录plan revision v1 可从 `governance_*` 事件投影只读时间线
- 缺口:`escalate`、正式计划 revision 表、治理审计链和对真实 ACP 执行流的硬中断仍不完整
- 建议:下一轮围绕治理审计和低风险 action policy把 route decision 到 governance command 的自动映射补齐。
5. **ManagedService 控制面**
- 已吸收Agent、MCP、Computer Server、Lanproxy、File Server、GUI Server 状态已进入 snapshot / projection。