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

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) {