吸收数字员工治理动作入口
This commit is contained in:
@@ -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
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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。
|
||||
|
||||
Reference in New Issue
Block a user