吸收数字员工入口路由策略下发
This commit is contained in:
@@ -29,6 +29,7 @@ import type {
|
||||
SkillSpec,
|
||||
CreatePlanRequest,
|
||||
RouteDecisionTimelineResponse,
|
||||
RouteDecisionPolicyPullResult,
|
||||
IngressRouteRequest,
|
||||
IngressRouteResponse,
|
||||
GovernanceCommandRequest,
|
||||
@@ -631,6 +632,12 @@ export function pullRiskApprovalPolicy(): Promise<{ ok: boolean; skipped?: boole
|
||||
});
|
||||
}
|
||||
|
||||
export function pullRouteDecisionPolicy(): Promise<RouteDecisionPolicyPullResult> {
|
||||
return apiFetch<RouteDecisionPolicyPullResult>('/api/route-decision/policy/pull', {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
export function getPlanSteps(params: Record<string, string | number | null | undefined> = {}): Promise<BusinessViewResponse<PlanStepBusinessRow>> {
|
||||
const query = new URLSearchParams();
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
|
||||
@@ -38,6 +38,8 @@ import type {
|
||||
PlanStepDispatchPolicy,
|
||||
PlanStepDispatchPolicyPullResult,
|
||||
PlanStepSummary,
|
||||
RouteDecisionPolicy,
|
||||
RouteDecisionPolicyPullResult,
|
||||
RouteDecisionRecord,
|
||||
RouteDecisionTimelineResponse,
|
||||
SkillSpec,
|
||||
@@ -59,6 +61,8 @@ declare global {
|
||||
pullApprovalUpdates?: () => Promise<{ ok?: boolean; applied?: number; ignored?: number; skipped?: boolean; error?: string | null }>;
|
||||
submitApprovalDecision?: (input: Record<string, unknown>) => Promise<{ ok?: boolean; error?: string | null }>;
|
||||
evaluateRiskPolicy?: (input: QimingclawRiskPolicyInput) => Promise<QimingclawRiskPolicyResult>;
|
||||
pullRouteDecisionPolicy?: () => Promise<RouteDecisionPolicyPullResult>;
|
||||
evaluateRouteDecisionPolicy?: (input: QimingclawRouteDecisionPolicyInput) => Promise<QimingclawRouteDecisionPolicyResult>;
|
||||
getPlanTemplates?: () => Promise<QimingclawPlanTemplateList>;
|
||||
listMemories?: (options?: { query?: string; category?: string; limit?: number }) => Promise<QimingclawMemoryRecord[]>;
|
||||
addMemory?: (input: QimingclawMemoryUpsertInput) => Promise<QimingclawMemoryRecord | null>;
|
||||
@@ -100,6 +104,7 @@ interface AdapterState {
|
||||
notificationPreferences?: NotificationPreferences;
|
||||
approvalSubscriptionPreferences?: ApprovalSubscriptionPreferences;
|
||||
planStepDispatchPolicy?: PlanStepDispatchPolicy;
|
||||
routeDecisionPolicy?: RouteDecisionPolicy;
|
||||
consoleRole?: string;
|
||||
rolePreferences?: Record<string, unknown>;
|
||||
profile: {
|
||||
@@ -360,6 +365,10 @@ interface QimingclawRouteDecisionInput {
|
||||
createsTaskRun?: boolean;
|
||||
approvalMode?: string | null;
|
||||
policyResult?: string | null;
|
||||
policySource?: string | null;
|
||||
blockedReason?: string | null;
|
||||
approvalId?: string | null;
|
||||
matchedIntent?: string | null;
|
||||
entrypoint?: string | null;
|
||||
actor?: string | null;
|
||||
message?: string | null;
|
||||
@@ -367,6 +376,38 @@ interface QimingclawRouteDecisionInput {
|
||||
recordedAt?: string | null;
|
||||
}
|
||||
|
||||
interface QimingclawRouteDecisionPolicyInput {
|
||||
routeKind?: string | null;
|
||||
intentKind?: string | null;
|
||||
reasonCodes?: string[] | null;
|
||||
candidateSkills?: string[] | null;
|
||||
contextRefs?: Record<string, unknown> | null;
|
||||
correlationId?: string | null;
|
||||
idempotencyKey?: string | null;
|
||||
entrypoint?: string | null;
|
||||
actor?: string | null;
|
||||
source?: string | null;
|
||||
payload?: Record<string, unknown> | null;
|
||||
occurredAt?: string | null;
|
||||
}
|
||||
|
||||
interface QimingclawRouteDecisionPolicyResult {
|
||||
ok?: boolean;
|
||||
decision?: 'allowed' | 'approval_required' | 'blocked' | string;
|
||||
status?: 'allowed' | 'approval_required' | 'blocked' | string;
|
||||
policy_result?: 'accepted' | 'approval_required' | 'blocked' | string;
|
||||
route_kind?: string;
|
||||
intent_kind?: string;
|
||||
reason_codes?: string[];
|
||||
matched_intent?: string | null;
|
||||
blocked_reason?: string | null;
|
||||
approval_id?: string | null;
|
||||
event_id?: string | null;
|
||||
policy_source?: string | null;
|
||||
policy?: RouteDecisionPolicy;
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
interface QimingclawRiskPolicyInput {
|
||||
actionKind?: string | null;
|
||||
actionLabel?: string | null;
|
||||
@@ -1022,6 +1063,9 @@ export async function handleQimingclawDigitalApi<T>(
|
||||
if (method === 'POST' && pathname === '/api/risk-approval/policy/pull') {
|
||||
return await pullBridgeRiskApprovalPolicy() as T;
|
||||
}
|
||||
if (method === 'POST' && pathname === '/api/route-decision/policy/pull') {
|
||||
return await pullBridgeRouteDecisionPolicy() as T;
|
||||
}
|
||||
if (method === 'POST' && pathname === '/api/approval/updates/pull') {
|
||||
return await pullBridgeApprovalUpdates() as T;
|
||||
}
|
||||
@@ -1045,6 +1089,7 @@ function defaultState(): AdapterState {
|
||||
notificationPreferences: defaultNotificationPreferences(),
|
||||
approvalSubscriptionPreferences: defaultApprovalSubscriptionPreferences(),
|
||||
planStepDispatchPolicy: defaultPlanStepDispatchPolicy(),
|
||||
routeDecisionPolicy: defaultRouteDecisionPolicy(),
|
||||
consoleRole: 'sales',
|
||||
rolePreferences: {},
|
||||
profile: {
|
||||
@@ -1072,6 +1117,7 @@ function normalizeAdapterState(value: Partial<AdapterState>): AdapterState {
|
||||
notificationPreferences: notificationPreferences(value),
|
||||
approvalSubscriptionPreferences: approvalSubscriptionPreferences(value),
|
||||
planStepDispatchPolicy: planStepDispatchPolicy(value),
|
||||
routeDecisionPolicy: routeDecisionPolicy(value),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1177,6 +1223,27 @@ async function pullBridgeRiskApprovalPolicy(): Promise<{ ok: boolean; skipped?:
|
||||
}
|
||||
}
|
||||
|
||||
async function pullBridgeRouteDecisionPolicy(): Promise<RouteDecisionPolicyPullResult> {
|
||||
const fallbackPolicy = routeDecisionPolicy((await readBridgeUiState()) ?? readState());
|
||||
const bridge = window.QimingClawBridge?.digital;
|
||||
if (!bridge?.pullRouteDecisionPolicy) {
|
||||
return { ok: false, policy: fallbackPolicy, error: 'qimingclaw_bridge_unavailable' };
|
||||
}
|
||||
try {
|
||||
const result = await bridge.pullRouteDecisionPolicy();
|
||||
return {
|
||||
ok: result?.ok !== false,
|
||||
skipped: result?.skipped,
|
||||
endpoint: result?.endpoint ?? null,
|
||||
pulled_at: result?.pulled_at ?? null,
|
||||
policy: routeDecisionPolicy({ routeDecisionPolicy: result?.policy ?? fallbackPolicy }),
|
||||
error: result?.error ?? null,
|
||||
};
|
||||
} catch (error) {
|
||||
return { ok: false, policy: fallbackPolicy, error: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
}
|
||||
|
||||
async function pullBridgeApprovalUpdates(): Promise<{ ok: boolean; applied?: number; ignored?: number; skipped?: boolean; error?: string | null }> {
|
||||
const bridge = window.QimingClawBridge?.digital;
|
||||
if (!bridge?.pullApprovalUpdates) {
|
||||
@@ -1223,6 +1290,39 @@ async function evaluateBridgeRiskPolicy(input: QimingclawRiskPolicyInput): Promi
|
||||
}
|
||||
}
|
||||
|
||||
async function evaluateBridgeRouteDecisionPolicy(input: QimingclawRouteDecisionPolicyInput): Promise<QimingclawRouteDecisionPolicyResult> {
|
||||
const bridge = window.QimingClawBridge?.digital;
|
||||
if (!bridge?.evaluateRouteDecisionPolicy) {
|
||||
return {
|
||||
ok: true,
|
||||
decision: 'allowed',
|
||||
status: 'allowed',
|
||||
policy_result: 'accepted',
|
||||
route_kind: input.routeKind || 'ChatOnly',
|
||||
intent_kind: normalizeRouteIntentKind(input.intentKind),
|
||||
reason_codes: ['qimingclaw_bridge_unavailable'],
|
||||
policy_source: 'local',
|
||||
policy: defaultRouteDecisionPolicy(),
|
||||
};
|
||||
}
|
||||
try {
|
||||
return await bridge.evaluateRouteDecisionPolicy(input);
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: true,
|
||||
decision: 'allowed',
|
||||
status: 'allowed',
|
||||
policy_result: 'accepted',
|
||||
route_kind: input.routeKind || 'ChatOnly',
|
||||
intent_kind: normalizeRouteIntentKind(input.intentKind),
|
||||
reason_codes: ['route_policy_evaluation_failed'],
|
||||
policy_source: 'local',
|
||||
policy: defaultRouteDecisionPolicy(),
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function riskPolicyBlockedResponse(result: QimingclawRiskPolicyResult): Record<string, unknown> | null {
|
||||
if (result.decision === 'allowed') return null;
|
||||
return {
|
||||
@@ -3097,6 +3197,10 @@ function routeDecisionFromRuntimePayload(payload: Record<string, unknown>, event
|
||||
creates_task_run: Boolean(decision.creates_task_run ?? decision.createsTaskRun),
|
||||
approval_mode: stringValue(decision.approval_mode ?? decision.approvalMode) || null,
|
||||
policy_result: stringValue(decision.policy_result ?? decision.policyResult) || 'accepted',
|
||||
policy_source: stringValue(decision.policy_source ?? decision.policySource) || null,
|
||||
blocked_reason: stringValue(decision.blocked_reason ?? decision.blockedReason) || null,
|
||||
approval_id: stringValue(decision.approval_id ?? decision.approvalId) || null,
|
||||
matched_intent: stringValue(decision.matched_intent ?? decision.matchedIntent) || null,
|
||||
recorded_at: stringValue(decision.recorded_at ?? decision.recordedAt) || event.occurredAt,
|
||||
entrypoint: stringValue(decision.entrypoint) || null,
|
||||
actor: stringValue(decision.actor) || null,
|
||||
@@ -3104,6 +3208,7 @@ function routeDecisionFromRuntimePayload(payload: Record<string, unknown>, event
|
||||
}
|
||||
|
||||
function routeDecisionAttentionLevel(decision: RouteDecisionRecord): 'info' | 'action' | 'warn' | 'error' {
|
||||
if (decision.policy_result === 'approval_required') return 'action';
|
||||
if (decision.policy_result && decision.policy_result !== 'accepted') return 'error';
|
||||
if (decision.reason_codes.includes('candidate_skills_disabled') || decision.reason_codes.includes('route_action_missing_context')) return 'warn';
|
||||
if (decision.created_command_id) return 'action';
|
||||
@@ -3113,6 +3218,7 @@ function routeDecisionAttentionLevel(decision: RouteDecisionRecord): 'info' | 'a
|
||||
function routeDecisionStatusLabel(decision: RouteDecisionRecord): string {
|
||||
const attentionLevel = routeDecisionAttentionLevel(decision);
|
||||
if (attentionLevel === 'error') return '策略拒绝';
|
||||
if (decision.policy_result === 'approval_required') return '待审批';
|
||||
if (attentionLevel === 'warn') return '需要关注';
|
||||
if (decision.created_command_id) return '已映射命令';
|
||||
return '已记录';
|
||||
@@ -3121,10 +3227,16 @@ function routeDecisionStatusLabel(decision: RouteDecisionRecord): string {
|
||||
function buildRouteDecisionSummary(snapshot: QimingclawSnapshot | null): DigitalEmployeeRouteSummary {
|
||||
const rows = routeDecisionRows(snapshot);
|
||||
const latest = rows[0] ?? null;
|
||||
const policy = routeDecisionPolicy(snapshot?.uiState ?? readState());
|
||||
return {
|
||||
total: rows.length,
|
||||
mapped_count: rows.filter((row) => stringValue(row.created_command_id)).length,
|
||||
attention_count: rows.filter((row) => ['warn', 'error'].includes(stringValue(row.attention_level))).length,
|
||||
blocked_count: rows.filter((row) => stringValue(row.policy_result) === 'blocked').length,
|
||||
approval_required_count: rows.filter((row) => stringValue(row.policy_result) === 'approval_required').length,
|
||||
policy_source: policy.source ?? null,
|
||||
policy_last_pulled_at: policy.last_pulled_at ?? null,
|
||||
policy_last_pull_error: policy.last_pull_error ?? null,
|
||||
latest: latest
|
||||
? {
|
||||
id: stringValue(latest.route_decision_id),
|
||||
@@ -3133,6 +3245,9 @@ function buildRouteDecisionSummary(snapshot: QimingclawSnapshot | null): Digital
|
||||
status_label: stringValue(latest.status_label),
|
||||
attention_level: stringValue(latest.attention_level),
|
||||
created_command_id: stringValue(latest.created_command_id) || null,
|
||||
policy_source: stringValue(latest.policy_source) || null,
|
||||
blocked_reason: stringValue(latest.blocked_reason) || null,
|
||||
approval_id: stringValue(latest.approval_id) || null,
|
||||
recorded_at: stringValue(latest.recorded_at),
|
||||
}
|
||||
: null,
|
||||
@@ -4152,6 +4267,21 @@ function defaultPlanStepDispatchPolicy(): PlanStepDispatchPolicy {
|
||||
};
|
||||
}
|
||||
|
||||
function defaultRouteDecisionPolicy(): RouteDecisionPolicy {
|
||||
return {
|
||||
enabled: true,
|
||||
auto_create_governance_commands: true,
|
||||
approval_required_intents: [],
|
||||
blocked_intents: [],
|
||||
preferred_route_kinds: [],
|
||||
updated_at: null,
|
||||
source: 'local',
|
||||
remote_updated_at: null,
|
||||
last_pulled_at: null,
|
||||
last_pull_error: null,
|
||||
};
|
||||
}
|
||||
|
||||
function approvalSubscriptionPreferences(state: Partial<AdapterState> | null | undefined): ApprovalSubscriptionPreferences {
|
||||
const stored = state?.approvalSubscriptionPreferences;
|
||||
const fallback = defaultApprovalSubscriptionPreferences();
|
||||
@@ -4188,6 +4318,28 @@ function planStepDispatchPolicy(state: Partial<AdapterState> | null | undefined)
|
||||
};
|
||||
}
|
||||
|
||||
function routeDecisionPolicy(state: Partial<AdapterState> | null | undefined): RouteDecisionPolicy {
|
||||
const stored = state?.routeDecisionPolicy;
|
||||
const fallback = defaultRouteDecisionPolicy();
|
||||
if (!stored || typeof stored !== 'object') return fallback;
|
||||
return {
|
||||
enabled: stored.enabled !== false,
|
||||
auto_create_governance_commands: stored.auto_create_governance_commands !== false,
|
||||
approval_required_intents: uniqueStrings(arrayValue(stored.approval_required_intents).map(normalizeRouteIntentKind)),
|
||||
blocked_intents: uniqueStrings(arrayValue(stored.blocked_intents).map(normalizeRouteIntentKind)),
|
||||
preferred_route_kinds: uniqueStrings(arrayValue(stored.preferred_route_kinds)),
|
||||
updated_at: typeof stored.updated_at === 'string' ? stored.updated_at : fallback.updated_at,
|
||||
source: stored.source === 'remote' ? 'remote' : 'local',
|
||||
remote_updated_at: typeof stored.remote_updated_at === 'string' ? stored.remote_updated_at : null,
|
||||
last_pulled_at: typeof stored.last_pulled_at === 'string' ? stored.last_pulled_at : null,
|
||||
last_pull_error: typeof stored.last_pull_error === 'string' ? stored.last_pull_error : null,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRouteIntentKind(value: unknown): string {
|
||||
return stringValue(value).trim().toLowerCase().replace(/[^a-z0-9_.:-]+/g, '_') || 'chat';
|
||||
}
|
||||
|
||||
function notificationPreferences(state: Partial<AdapterState> | null | undefined): NotificationPreferences {
|
||||
const stored = state?.notificationPreferences;
|
||||
const fallback = defaultNotificationPreferences();
|
||||
@@ -5720,11 +5872,50 @@ async function handleIngressRoute(
|
||||
creates_task_run: classification.createsTaskRun,
|
||||
approval_mode: null,
|
||||
policy_result: 'accepted',
|
||||
policy_source: null,
|
||||
blocked_reason: null,
|
||||
approval_id: null,
|
||||
matched_intent: null,
|
||||
recorded_at: recordedAt,
|
||||
entrypoint: body.entrypoint ?? null,
|
||||
actor: body.actor ?? null,
|
||||
};
|
||||
const routeCommand = buildGovernanceCommandForRouteDecision(decision, body, snapshot);
|
||||
const routePolicy = await evaluateBridgeRouteDecisionPolicy({
|
||||
routeKind: decision.route_kind,
|
||||
intentKind: decision.intent_kind,
|
||||
reasonCodes: decision.reason_codes,
|
||||
candidateSkills: decision.candidate_skills,
|
||||
contextRefs: decision.context_refs,
|
||||
correlationId,
|
||||
idempotencyKey: body.idempotency_key,
|
||||
entrypoint: body.entrypoint ?? null,
|
||||
actor: body.actor ?? null,
|
||||
source: 'qimingclaw-ingress-route',
|
||||
payload: {
|
||||
text: body.text ?? null,
|
||||
payload: body.payload ?? null,
|
||||
attachments: body.attachments ?? [],
|
||||
},
|
||||
occurredAt: recordedAt,
|
||||
});
|
||||
const routePolicyDecision = stringValue(routePolicy.decision) || 'allowed';
|
||||
decision.policy_result = routePolicy.policy_result || (routePolicyDecision === 'allowed' ? 'accepted' : routePolicyDecision) || 'accepted';
|
||||
decision.policy_source = routePolicy.policy_source ?? routePolicy.policy?.source ?? null;
|
||||
decision.blocked_reason = routePolicy.blocked_reason ?? null;
|
||||
decision.approval_id = routePolicy.approval_id ?? null;
|
||||
decision.matched_intent = routePolicy.matched_intent ?? null;
|
||||
decision.reason_codes = uniqueStrings([...decision.reason_codes, ...(routePolicy.reason_codes ?? [])]);
|
||||
const preferredKinds = routePolicy.policy?.preferred_route_kinds ?? [];
|
||||
if (preferredKinds.length > 0 && !preferredKinds.includes(decision.route_kind)) {
|
||||
decision.reason_codes = uniqueStrings([...decision.reason_codes, 'route_kind_not_preferred']);
|
||||
}
|
||||
const shouldExecuteCommand = routePolicyDecision === 'allowed' && routePolicy.policy?.auto_create_governance_commands !== false;
|
||||
if (routePolicyDecision === 'allowed' && routePolicy.policy?.auto_create_governance_commands === false) {
|
||||
decision.reason_codes = uniqueStrings([...decision.reason_codes, 'route_command_auto_create_disabled']);
|
||||
}
|
||||
const routeCommand = shouldExecuteCommand
|
||||
? buildGovernanceCommandForRouteDecision(decision, body, snapshot)
|
||||
: { command: null, reasonCode: routePolicyDecision === 'allowed' ? 'route_command_auto_create_disabled' : `route_policy_${decision.policy_result}` };
|
||||
const command = routeCommand.command
|
||||
? await handleGovernanceCommand(routeCommand.command, snapshot)
|
||||
: null;
|
||||
@@ -5750,6 +5941,10 @@ async function handleIngressRoute(
|
||||
createsTaskRun: decision.creates_task_run,
|
||||
approvalMode: decision.approval_mode,
|
||||
policyResult: decision.policy_result,
|
||||
policySource: decision.policy_source,
|
||||
blockedReason: decision.blocked_reason,
|
||||
approvalId: decision.approval_id,
|
||||
matchedIntent: decision.matched_intent,
|
||||
entrypoint: decision.entrypoint,
|
||||
actor: decision.actor,
|
||||
message: `入口路由决策:${decision.route_kind} / ${decision.intent_kind}`,
|
||||
@@ -5763,6 +5958,31 @@ async function handleIngressRoute(
|
||||
: null;
|
||||
const routeDecision = recorded ?? decision;
|
||||
const resultIds = routeCommandResultIds(command);
|
||||
if (routePolicyDecision !== 'allowed') {
|
||||
return {
|
||||
accepted: false,
|
||||
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,
|
||||
error: 'policy_blocked',
|
||||
message: routePolicyDecision === 'approval_required'
|
||||
? `入口路由需要审批:${routeDecision.route_kind} / ${routeDecision.intent_kind}`
|
||||
: `入口路由已被策略阻断:${routeDecision.route_kind} / ${routeDecision.intent_kind}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
accepted: true,
|
||||
envelope: {
|
||||
|
||||
Reference in New Issue
Block a user