吸收数字员工入口路由决策
This commit is contained in:
@@ -22,9 +22,13 @@ import type {
|
||||
FlowResponse,
|
||||
GovernanceCommandRequest,
|
||||
GovernanceCommandResponse,
|
||||
IngressRouteRequest,
|
||||
IngressRouteResponse,
|
||||
MemoryEntry,
|
||||
PlanDispatchResponse,
|
||||
PlanStepSummary,
|
||||
RouteDecisionRecord,
|
||||
RouteDecisionTimelineResponse,
|
||||
SkillSpec,
|
||||
} from '@/types/api';
|
||||
|
||||
@@ -49,6 +53,8 @@ declare global {
|
||||
runSchedulerCheck?: () => Promise<{ activated: number; due?: number; skipped?: number }>;
|
||||
runScheduleNow?: (scheduleId: string) => Promise<{ ok?: boolean; scheduleRunId?: string; error?: string }>;
|
||||
getScheduleRuns?: (scheduleId: string, limit?: number) => Promise<QimingclawScheduleRunRecord[]>;
|
||||
listRouteDecisions?: (options?: { limit?: number }) => Promise<RouteDecisionRecord[]>;
|
||||
recordRouteDecision?: (input: QimingclawRouteDecisionInput) => Promise<RouteDecisionRecord>;
|
||||
recordGovernanceCommand?: (command: QimingclawGovernanceCommandRecord) => Promise<unknown>;
|
||||
respondPermission?: (sessionId: string, permissionId: string, response: 'once' | 'always' | 'reject') => Promise<{ success?: boolean; error?: string }>;
|
||||
};
|
||||
@@ -189,6 +195,27 @@ interface QimingclawGovernanceCommandRecord {
|
||||
result?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
interface QimingclawRouteDecisionInput {
|
||||
id?: string | null;
|
||||
idempotencyKey?: string | null;
|
||||
correlationId?: string | null;
|
||||
routeKind: string;
|
||||
intentKind: string;
|
||||
reasonCodes?: string[];
|
||||
candidateSkills?: string[];
|
||||
contextRefs?: Record<string, unknown>;
|
||||
createdCommandId?: string | null;
|
||||
createsPlan?: boolean;
|
||||
createsTaskRun?: boolean;
|
||||
approvalMode?: string | null;
|
||||
policyResult?: string | null;
|
||||
entrypoint?: string | null;
|
||||
actor?: string | null;
|
||||
message?: string | null;
|
||||
envelope?: Record<string, unknown> | null;
|
||||
recordedAt?: string | null;
|
||||
}
|
||||
|
||||
interface QimingclawRuntimeRecords {
|
||||
generatedAt: string;
|
||||
plans: QimingclawPlanRecord[];
|
||||
@@ -552,6 +579,12 @@ export async function handleQimingclawDigitalApi<T>(
|
||||
if (method === 'POST' && pathname === '/api/digital-employee/workday/actions') {
|
||||
return await handleWorkdayAction(parseJsonBody<DigitalEmployeeWorkdayActionRequest>(options.body), await readQimingclawSnapshot()) as T;
|
||||
}
|
||||
if (method === 'GET' && pathname === '/api/route-decisions') {
|
||||
return await readRouteDecisions(Number(url.searchParams.get('limit')) || 80) as T;
|
||||
}
|
||||
if (method === 'POST' && pathname === '/api/ingress/route') {
|
||||
return await handleIngressRoute(parseJsonBody<IngressRouteRequest>(options.body), await readQimingclawSnapshot()) as T;
|
||||
}
|
||||
if (method === 'POST' && pathname === '/api/governance/command') {
|
||||
return await handleGovernanceCommand(parseJsonBody<GovernanceCommandRequest>(options.body), await readQimingclawSnapshot()) as T;
|
||||
}
|
||||
@@ -2582,6 +2615,189 @@ function acpResponseForDecision(decision: string): 'once' | 'always' | 'reject'
|
||||
return 'once';
|
||||
}
|
||||
|
||||
async function readRouteDecisions(limit = 80): Promise<RouteDecisionTimelineResponse> {
|
||||
const bridge = window.QimingClawBridge?.digital;
|
||||
if (!bridge?.listRouteDecisions) {
|
||||
return { has_route_decision_state: false, decisions: [] };
|
||||
}
|
||||
try {
|
||||
const decisions = await bridge.listRouteDecisions({ limit });
|
||||
return { has_route_decision_state: true, decisions: decisions ?? [] };
|
||||
} catch (error) {
|
||||
console.warn('[qimingclawAdapter] list route decisions failed:', error);
|
||||
return { has_route_decision_state: false, decisions: [] };
|
||||
}
|
||||
}
|
||||
|
||||
async function handleIngressRoute(
|
||||
body: IngressRouteRequest,
|
||||
snapshot: QimingclawSnapshot | null,
|
||||
): Promise<IngressRouteResponse> {
|
||||
const recordedAt = body.timestamp || new Date().toISOString();
|
||||
const correlationId = body.correlation_id || `route-${Date.now()}`;
|
||||
const idSource = body.idempotency_key || correlationId;
|
||||
const contextRefs = buildIngressContextRefs(body);
|
||||
const classification = classifyIngressRoute(body, contextRefs, snapshot);
|
||||
const decision: RouteDecisionRecord = {
|
||||
id: `route-decision-${slugifyIdPart(idSource)}`,
|
||||
route_kind: classification.routeKind,
|
||||
intent_kind: classification.intentKind,
|
||||
reason_codes: classification.reasonCodes,
|
||||
candidate_skills: classification.candidateSkills,
|
||||
context_refs: contextRefs,
|
||||
created_command_id: null,
|
||||
correlation_id: correlationId,
|
||||
creates_plan: classification.createsPlan,
|
||||
creates_task_run: classification.createsTaskRun,
|
||||
approval_mode: null,
|
||||
policy_result: 'accepted',
|
||||
recorded_at: recordedAt,
|
||||
entrypoint: body.entrypoint ?? null,
|
||||
actor: body.actor ?? null,
|
||||
};
|
||||
const bridge = window.QimingClawBridge?.digital;
|
||||
const recorded = bridge?.recordRouteDecision
|
||||
? await bridge.recordRouteDecision({
|
||||
id: decision.id,
|
||||
idempotencyKey: body.idempotency_key,
|
||||
correlationId,
|
||||
routeKind: decision.route_kind,
|
||||
intentKind: decision.intent_kind,
|
||||
reasonCodes: decision.reason_codes,
|
||||
candidateSkills: decision.candidate_skills,
|
||||
contextRefs: decision.context_refs,
|
||||
createdCommandId: decision.created_command_id,
|
||||
createsPlan: decision.creates_plan,
|
||||
createsTaskRun: decision.creates_task_run,
|
||||
approvalMode: decision.approval_mode,
|
||||
policyResult: decision.policy_result,
|
||||
entrypoint: decision.entrypoint,
|
||||
actor: decision.actor,
|
||||
message: `入口路由决策:${decision.route_kind} / ${decision.intent_kind}`,
|
||||
envelope: {
|
||||
text: body.text ?? null,
|
||||
payload: body.payload ?? null,
|
||||
attachments: body.attachments ?? [],
|
||||
},
|
||||
recordedAt,
|
||||
})
|
||||
: null;
|
||||
const routeDecision = recorded ?? decision;
|
||||
return {
|
||||
accepted: true,
|
||||
envelope: {
|
||||
entrypoint: body.entrypoint ?? 'chat',
|
||||
text: body.text ?? null,
|
||||
payload: body.payload ?? null,
|
||||
attachments: body.attachments ?? [],
|
||||
context_refs: contextRefs,
|
||||
},
|
||||
response_kind: 'route_decision',
|
||||
route_decision: routeDecision,
|
||||
command: null,
|
||||
created_plan_id: null,
|
||||
created_schedule_id: null,
|
||||
created_task_ids: [],
|
||||
created_run_ids: [],
|
||||
projection_name: null,
|
||||
projection_payload: null,
|
||||
message: `已记录入口路由:${routeDecision.route_kind} / ${routeDecision.intent_kind}`,
|
||||
};
|
||||
}
|
||||
|
||||
function buildIngressContextRefs(body: IngressRouteRequest): Record<string, unknown> {
|
||||
const payload = asRecord(body.payload) ?? {};
|
||||
const context = asRecord(body.context_refs) ?? {};
|
||||
return compactRecord({
|
||||
...context,
|
||||
entrypoint: body.entrypoint ?? context.entrypoint,
|
||||
plan_id: stringValue(context.plan_id) || stringValue(payload.plan_id) || stringValue(payload.planId),
|
||||
task_id: stringValue(context.task_id) || stringValue(payload.task_id) || stringValue(payload.taskId),
|
||||
run_id: stringValue(context.run_id) || stringValue(payload.run_id) || stringValue(payload.runId),
|
||||
step_id: stringValue(context.step_id) || stringValue(payload.step_id) || stringValue(payload.stepId),
|
||||
schedule_id: stringValue(context.schedule_id) || stringValue(payload.schedule_id) || stringValue(payload.scheduleId),
|
||||
});
|
||||
}
|
||||
|
||||
function classifyIngressRoute(
|
||||
body: IngressRouteRequest,
|
||||
contextRefs: Record<string, unknown>,
|
||||
snapshot: QimingclawSnapshot | null,
|
||||
): { routeKind: string; intentKind: string; reasonCodes: string[]; candidateSkills: string[]; createsPlan: boolean; createsTaskRun: boolean } {
|
||||
const payload = asRecord(body.payload) ?? {};
|
||||
const text = `${body.text ?? ''} ${stringValue(payload.action)} ${stringValue(payload.command)} ${stringValue(payload.intent)}`.toLowerCase();
|
||||
const hasPayload = Object.keys(payload).length > 0;
|
||||
const hasContext = ['plan_id', 'task_id', 'step_id', 'schedule_id'].some((key) => Boolean(stringValue(contextRefs[key])));
|
||||
const stepId = stringValue(contextRefs.step_id);
|
||||
const readyStep = stepId && runtimePlanSteps(snapshot).some((step) => step.id === stepId && step.status.toLowerCase() === 'ready');
|
||||
|
||||
if (hasContext && matchesRouteText(text, ['start', 'run', 'retry', 'skip', 'cancel', 'pause', 'resume', 'approve', '启动', '开始', '执行', '重试', '跳过', '取消', '暂停', '恢复', '批准'])) {
|
||||
return routeClassification('PlanControl', 'control', ['context_control_action'], contextRefs, payload, false, false);
|
||||
}
|
||||
if (body.entrypoint === 'cron' || matchesRouteText(text, ['cron', 'schedule', 'scheduled', '定时', '调度', '周期'])) {
|
||||
return routeClassification('PlanExecution', 'schedule', ['scheduled_entrypoint'], contextRefs, payload, false, true);
|
||||
}
|
||||
if (matchesRouteText(text, ['status', 'progress', 'result', 'report', 'artifact', 'memory', '状态', '进度', '结果', '日报', '产物', '记忆'])) {
|
||||
return routeClassification('ProjectionQuery', 'query', ['projection_query_text'], contextRefs, payload, false, false);
|
||||
}
|
||||
if (readyStep || matchesRouteText(text, ['execute', 'dispatch', 'delegate', 'run task', '执行', '开始', '委托', '派发'])) {
|
||||
return routeClassification('PlanExecution', 'execute', readyStep ? ['ready_step_context'] : ['execution_text'], contextRefs, payload, false, true);
|
||||
}
|
||||
if (!body.text && !hasPayload && !hasContext) {
|
||||
return routeClassification('AskClarification', 'chat', ['empty_ingress'], contextRefs, payload, false, false);
|
||||
}
|
||||
return routeClassification('ChatOnly', 'chat', ['default_chat'], contextRefs, payload, false, false);
|
||||
}
|
||||
|
||||
function routeClassification(
|
||||
routeKind: string,
|
||||
intentKind: string,
|
||||
reasonCodes: string[],
|
||||
contextRefs: Record<string, unknown>,
|
||||
payload: Record<string, unknown>,
|
||||
createsPlan: boolean,
|
||||
createsTaskRun: boolean,
|
||||
): { routeKind: string; intentKind: string; reasonCodes: string[]; candidateSkills: string[]; createsPlan: boolean; createsTaskRun: boolean } {
|
||||
return {
|
||||
routeKind,
|
||||
intentKind,
|
||||
reasonCodes,
|
||||
candidateSkills: routeCandidateSkills(routeKind, contextRefs, payload),
|
||||
createsPlan,
|
||||
createsTaskRun,
|
||||
};
|
||||
}
|
||||
|
||||
function routeCandidateSkills(routeKind: string, contextRefs: Record<string, unknown>, payload: Record<string, unknown>): string[] {
|
||||
const explicit = uniqueStrings([
|
||||
...arrayValue(contextRefs.skill_ids),
|
||||
...arrayValue(contextRefs.skills),
|
||||
...arrayValue(contextRefs.candidate_skills),
|
||||
...arrayValue(payload.skill_ids),
|
||||
...arrayValue(payload.skills),
|
||||
...arrayValue(payload.candidate_skills),
|
||||
stringValue(contextRefs.skill_id),
|
||||
stringValue(payload.skill_id),
|
||||
]);
|
||||
if (explicit.length > 0) return explicit;
|
||||
if (routeKind === 'PlanControl') return ['qimingclaw-task-agent'];
|
||||
if (routeKind === 'ProjectionQuery') return ['qimingclaw-artifact-report'];
|
||||
if (routeKind === 'PlanExecution') return ['qimingclaw-computer-control'];
|
||||
return [];
|
||||
}
|
||||
|
||||
function uniqueStrings(values: unknown[]): string[] {
|
||||
return [...new Set(values.map(stringValue).filter(Boolean))];
|
||||
}
|
||||
|
||||
function matchesRouteText(text: string, keywords: string[]): boolean {
|
||||
return keywords.some((keyword) => text.includes(keyword.toLowerCase()));
|
||||
}
|
||||
|
||||
function compactRecord(record: Record<string, unknown>): Record<string, unknown> {
|
||||
return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined && value !== null && value !== ''));
|
||||
}
|
||||
|
||||
async function handleGovernanceCommand(
|
||||
body: GovernanceCommandRequest,
|
||||
snapshot: QimingclawSnapshot | null,
|
||||
|
||||
Reference in New Issue
Block a user