吸收数字员工入口路由决策

This commit is contained in:
baiyanyun
2026-06-09 15:32:38 +08:00
parent fd130f1f60
commit 702ea05d4d
9 changed files with 598 additions and 144 deletions

View File

@@ -186,6 +186,45 @@ export interface DigitalEmployeeEventRecord {
createdAt: string;
}
export interface DigitalEmployeeRouteDecisionRecord {
id: string;
route_kind: string;
intent_kind: string;
reason_codes: string[];
candidate_skills: string[];
context_refs: Record<string, unknown>;
created_command_id: string | null;
correlation_id: string;
creates_plan: boolean;
creates_task_run: boolean;
approval_mode: string | null;
policy_result: string | null;
recorded_at: string;
entrypoint?: string | null;
actor?: string | null;
}
export interface DigitalEmployeeRouteDecisionInput {
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;
}
export interface DigitalEmployeeArtifactRecord {
id: string;
remoteId?: string | null;
@@ -773,6 +812,96 @@ export function readDigitalEmployeeRuntimeRecords(
};
}
export function recordDigitalEmployeeRouteDecision(
input: DigitalEmployeeRouteDecisionInput,
): DigitalEmployeeRouteDecisionRecord {
const now = input.recordedAt || new Date().toISOString();
const idSource = stringValue(input.idempotencyKey) || stringValue(input.correlationId) || stringValue(input.id) || `${Date.now()}`;
const id = stringValue(input.id) || `route-decision:${safeId(idSource)}`;
const correlationId = stringValue(input.correlationId) || stringValue(input.idempotencyKey) || id;
const routeDecision: DigitalEmployeeRouteDecisionRecord = {
id,
route_kind: stringValue(input.routeKind) || "ChatOnly",
intent_kind: stringValue(input.intentKind) || "chat",
reason_codes: normalizeSkillIds(input.reasonCodes ?? []),
candidate_skills: normalizeSkillIds(input.candidateSkills ?? []),
context_refs: objectRecord(input.contextRefs) ?? {},
created_command_id: stringValue(input.createdCommandId) || null,
correlation_id: correlationId,
creates_plan: Boolean(input.createsPlan),
creates_task_run: Boolean(input.createsTaskRun),
approval_mode: stringValue(input.approvalMode) || null,
policy_result: stringValue(input.policyResult) || null,
recorded_at: now,
entrypoint: stringValue(input.entrypoint) || null,
actor: stringValue(input.actor) || null,
};
const db = getDigitalEmployeeDb();
if (!db) return routeDecision;
insertEvent(
{
event_id: id,
kind: "route_decision",
occurred_at: now,
message: input.message || routeDecisionMessage(routeDecision),
plan_id: null,
task_id: null,
},
null,
{
source: "qimingclaw-route-decision",
routeDecision,
envelope: objectRecord(input.envelope) ?? null,
},
);
return routeDecision;
}
export function listDigitalEmployeeRouteDecisions(options: { limit?: number } = {}): DigitalEmployeeRouteDecisionRecord[] {
const db = getDigitalEmployeeDb();
if (!db) return [];
const safeLimit = Math.max(1, Math.min(Math.floor(options.limit ?? 80), 300));
const rows = db.prepare(`
SELECT payload
FROM digital_events
WHERE kind = 'route_decision'
ORDER BY occurred_at DESC, created_at DESC
LIMIT ?
`).all(safeLimit) as Array<Record<string, unknown>>;
return rows
.map((row) => routeDecisionFromEventPayload(parseStoredPayload(row.payload)))
.filter((decision): decision is DigitalEmployeeRouteDecisionRecord => Boolean(decision));
}
function routeDecisionFromEventPayload(payload: unknown): DigitalEmployeeRouteDecisionRecord | null {
const record = objectRecord(payload);
const decision = objectRecord(record?.routeDecision ?? record?.route_decision ?? payload);
if (!decision) return null;
const id = stringValue(decision.id);
if (!id) return null;
return {
id,
route_kind: stringValue(decision.route_kind ?? decision.routeKind) || "ChatOnly",
intent_kind: stringValue(decision.intent_kind ?? decision.intentKind) || "chat",
reason_codes: parseStringArray(decision.reason_codes ?? decision.reasonCodes),
candidate_skills: parseStringArray(decision.candidate_skills ?? decision.candidateSkills),
context_refs: objectRecord(decision.context_refs ?? decision.contextRefs) ?? {},
created_command_id: stringValue(decision.created_command_id ?? decision.createdCommandId) || null,
correlation_id: stringValue(decision.correlation_id ?? decision.correlationId) || id,
creates_plan: Boolean(decision.creates_plan ?? decision.createsPlan),
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) || null,
recorded_at: stringValue(decision.recorded_at ?? decision.recordedAt) || new Date().toISOString(),
entrypoint: stringValue(decision.entrypoint) || null,
actor: stringValue(decision.actor) || null,
};
}
function routeDecisionMessage(decision: DigitalEmployeeRouteDecisionRecord): string {
return `入口路由决策:${decision.route_kind} / ${decision.intent_kind}`;
}
export function listDigitalEmployeeMemories(options: {
query?: string | null;
category?: string | null;