吸收数字员工审批历史订阅策略
This commit is contained in:
@@ -12,6 +12,7 @@ import type {
|
||||
Session,
|
||||
ChannelDetail,
|
||||
BrowserSession,
|
||||
ApprovalSubscriptionPreferences,
|
||||
NotificationPreferences,
|
||||
UserNotification,
|
||||
DebugStoreResponse,
|
||||
@@ -543,7 +544,7 @@ export function getEventLog(params: {
|
||||
// Notifications
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export { type NotificationPreferences, type UserNotification };
|
||||
export { type ApprovalSubscriptionPreferences, type NotificationPreferences, type UserNotification };
|
||||
|
||||
export function getNotifications(
|
||||
status: string = 'unread',
|
||||
@@ -572,6 +573,28 @@ export function patchNotificationPreferences(
|
||||
});
|
||||
}
|
||||
|
||||
export function getApprovalSubscriptionPreferences(): Promise<ApprovalSubscriptionPreferences> {
|
||||
return apiFetch<ApprovalSubscriptionPreferences>('/api/approval/subscriptions');
|
||||
}
|
||||
|
||||
export function patchApprovalSubscriptionPreferences(
|
||||
patch: Partial<ApprovalSubscriptionPreferences>,
|
||||
): Promise<ApprovalSubscriptionPreferences> {
|
||||
return apiFetch<ApprovalSubscriptionPreferences>('/api/approval/subscriptions', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(patch),
|
||||
});
|
||||
}
|
||||
|
||||
export function getApprovalHistory(params: Record<string, string | number | null | undefined> = {}): Promise<any> {
|
||||
const query = new URLSearchParams();
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && `${value}`.trim() !== '') query.set(key, `${value}`);
|
||||
});
|
||||
const suffix = query.toString();
|
||||
return apiFetch(`/api/digital-employee/approval-history${suffix ? `?${suffix}` : ''}`);
|
||||
}
|
||||
|
||||
export function markNotificationRead(id: string): Promise<boolean> {
|
||||
return apiFetch<{ ok: boolean }>(`/api/notifications/${encodeURIComponent(id)}/read`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
ApprovalEntry,
|
||||
ApprovalSubscriptionPreferences,
|
||||
ApprovalTimelineResponse,
|
||||
ArtifactEntry,
|
||||
CanonicalEvent,
|
||||
@@ -87,6 +88,7 @@ interface AdapterState {
|
||||
decisionResultsByDate: Record<string, Record<string, string>>;
|
||||
notificationStateById?: Record<string, NotificationState>;
|
||||
notificationPreferences?: NotificationPreferences;
|
||||
approvalSubscriptionPreferences?: ApprovalSubscriptionPreferences;
|
||||
consoleRole?: string;
|
||||
rolePreferences?: Record<string, unknown>;
|
||||
profile: {
|
||||
@@ -116,6 +118,8 @@ const NOTIFICATION_KINDS: UserNotification['kind'][] = [
|
||||
'confirmation_pending',
|
||||
'confirmation_resolved',
|
||||
];
|
||||
const APPROVAL_SUBSCRIPTION_ACTIONS: ApprovalSubscriptionPreferences['subscribed_actions'] = ['comment', 'delegate', 'mark_timeout', 'mark_risk', 'clear_risk', 'decision'];
|
||||
const APPROVAL_SUBSCRIPTION_RISK_LEVELS: ApprovalSubscriptionPreferences['subscribed_risk_levels'] = ['normal', 'medium', 'high', 'critical'];
|
||||
|
||||
interface QimingclawServiceStatus {
|
||||
running?: boolean;
|
||||
@@ -687,7 +691,7 @@ const PLAN_SCHEDULES: Record<string, { expression: string; nextRun: string; sche
|
||||
|
||||
const BUSINESS_VIEW_SOURCE = 'qimingclaw-runtime-projection';
|
||||
|
||||
type BusinessViewKind = 'plans' | 'tasks' | 'runs' | 'events' | 'artifacts' | 'approvals' | 'route-decisions' | 'notifications' | 'audits';
|
||||
type BusinessViewKind = 'plans' | 'tasks' | 'runs' | 'events' | 'artifacts' | 'approvals' | 'approval-history' | 'route-decisions' | 'notifications' | 'audits';
|
||||
type BusinessViewRow = Record<string, unknown>;
|
||||
|
||||
export function isQimingclawDigitalApiEnabled(): boolean {
|
||||
@@ -771,7 +775,20 @@ export async function handleQimingclawDigitalApi<T>(
|
||||
const approvalId = decodeURIComponent(approvalActionMatch[1] || '');
|
||||
return await handleApprovalAction(approvalId, parseOptionalJsonBody(options.body), await readQimingclawSnapshot()) as T;
|
||||
}
|
||||
const businessViewMatch = pathname.match(/^\/api\/digital-employee\/(plans|tasks|runs|events|artifacts|approvals|route-decisions|notifications|audits)$/);
|
||||
if (method === 'GET' && pathname === '/api/approval/subscriptions') {
|
||||
return approvalSubscriptionPreferences(notificationAdapterState(await readQimingclawSnapshot())) as T;
|
||||
}
|
||||
if (method === 'PATCH' && pathname === '/api/approval/subscriptions') {
|
||||
return await handleApprovalSubscriptionPreferencesPatch(
|
||||
parseJsonBody<Partial<ApprovalSubscriptionPreferences>>(options.body),
|
||||
await readQimingclawSnapshot(),
|
||||
) as T;
|
||||
}
|
||||
if (method === 'GET' && pathname === '/api/digital-employee/approval-history') {
|
||||
const rows = filterApprovalHistoryRows(approvalHistoryRows(await readQimingclawSnapshot()), url.searchParams);
|
||||
return { ...paginateBusinessRows(rows, url.searchParams), source: BUSINESS_VIEW_SOURCE } as T;
|
||||
}
|
||||
const businessViewMatch = pathname.match(/^\/api\/digital-employee\/(plans|tasks|runs|events|artifacts|approvals|approval-history|route-decisions|notifications|audits)$/);
|
||||
if (method === 'GET' && businessViewMatch) {
|
||||
return buildBusinessViewResponse(businessViewMatch[1] as BusinessViewKind, await readQimingclawSnapshot(), url.searchParams) as T;
|
||||
}
|
||||
@@ -946,6 +963,7 @@ function defaultState(): AdapterState {
|
||||
decisionResultsByDate: {},
|
||||
notificationStateById: {},
|
||||
notificationPreferences: defaultNotificationPreferences(),
|
||||
approvalSubscriptionPreferences: defaultApprovalSubscriptionPreferences(),
|
||||
consoleRole: 'sales',
|
||||
rolePreferences: {},
|
||||
profile: {
|
||||
@@ -971,6 +989,7 @@ function normalizeAdapterState(value: Partial<AdapterState>): AdapterState {
|
||||
...defaultState(),
|
||||
...value,
|
||||
notificationPreferences: notificationPreferences(value),
|
||||
approvalSubscriptionPreferences: approvalSubscriptionPreferences(value),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2506,17 +2525,20 @@ function buildBusinessViewResponse(kind: BusinessViewKind, snapshot: QimingclawS
|
||||
events: businessEventRows(snapshot),
|
||||
artifacts: businessArtifactRows(snapshot),
|
||||
approvals: businessApprovalRows(snapshot),
|
||||
'approval-history': approvalHistoryRows(snapshot),
|
||||
'route-decisions': routeDecisionRows(snapshot),
|
||||
notifications: notificationRows(snapshot, notificationAdapterState(snapshot)),
|
||||
audits: auditRows(snapshot),
|
||||
};
|
||||
const rows = kind === 'route-decisions'
|
||||
? filterRouteDecisionRows(rowsByKind[kind], searchParams)
|
||||
: kind === 'notifications'
|
||||
? filterNotificationRows(rowsByKind[kind], searchParams)
|
||||
: kind === 'audits'
|
||||
? filterAuditRows(rowsByKind[kind], searchParams)
|
||||
: filterBusinessRows(rowsByKind[kind], searchParams);
|
||||
: kind === 'approval-history'
|
||||
? filterApprovalHistoryRows(rowsByKind[kind], searchParams)
|
||||
: kind === 'notifications'
|
||||
? filterNotificationRows(rowsByKind[kind], searchParams)
|
||||
: kind === 'audits'
|
||||
? filterAuditRows(rowsByKind[kind], searchParams)
|
||||
: filterBusinessRows(rowsByKind[kind], searchParams);
|
||||
return {
|
||||
...paginateBusinessRows(rows, searchParams),
|
||||
source: BUSINESS_VIEW_SOURCE,
|
||||
@@ -2887,6 +2909,7 @@ function approvalActionRows(snapshot: QimingclawSnapshot | null): BusinessViewRo
|
||||
action: stringValue(payload.action) || null,
|
||||
status: stringValue(payload.status) || null,
|
||||
comment_preview: stringValue(comment?.preview) || null,
|
||||
comment_length: numberValue(comment?.length),
|
||||
delegate_to: stringValue(delegate?.label ?? delegate?.target) || null,
|
||||
sla_status: stringValue(sla?.sla_status) || null,
|
||||
due_at: stringValue(sla?.due_at) || null,
|
||||
@@ -2905,6 +2928,77 @@ function approvalActionRows(snapshot: QimingclawSnapshot | null): BusinessViewRo
|
||||
.sort((left, right) => stringValue(right.occurred_at).localeCompare(stringValue(left.occurred_at)));
|
||||
}
|
||||
|
||||
function approvalHistoryRows(snapshot: QimingclawSnapshot | null): BusinessViewRow[] {
|
||||
const approvalRows = new Map(businessApprovalRows(snapshot).map((approval) => [stringValue(approval.approval_id), approval]));
|
||||
const actionRows = approvalActionRows(snapshot).map((action) => {
|
||||
const approvalId = stringValue(action.approval_id);
|
||||
const approval = approvalRows.get(approvalId);
|
||||
return {
|
||||
history_id: stringValue(action.action_id),
|
||||
event_id: stringValue(action.event_id),
|
||||
approval_id: approvalId || null,
|
||||
action: stringValue(action.action) || null,
|
||||
status: stringValue(action.status) || null,
|
||||
actor: stringValue(action.actor) || null,
|
||||
risk_level: stringValue(action.risk_level) || null,
|
||||
due_at: stringValue(action.due_at) || null,
|
||||
delegate_to: stringValue(action.delegate_to) || null,
|
||||
comment_length: numberValue(action.comment_length),
|
||||
message: stringValue(action.message) || null,
|
||||
plan_id: stringValue(action.plan_id) || stringValue(approval?.plan_id) || null,
|
||||
task_id: stringValue(action.task_id) || stringValue(approval?.task_id) || null,
|
||||
run_id: stringValue(action.run_id) || stringValue(approval?.run_id) || null,
|
||||
occurred_at: stringValue(action.occurred_at) || null,
|
||||
updated_at: stringValue(action.occurred_at) || null,
|
||||
sync_status: stringValue(action.sync_status) || null,
|
||||
entity_type: 'approval_history',
|
||||
entity_id: stringValue(action.action_id),
|
||||
source: BUSINESS_VIEW_SOURCE,
|
||||
};
|
||||
});
|
||||
const decisionRows = runtimeEvents(snapshot)
|
||||
.filter((event) => event.kind === 'approval_decision')
|
||||
.map((event) => {
|
||||
const payload = asRecord(event.payload) ?? {};
|
||||
const approvalId = stringValue(payload.approvalId ?? payload.approval_id) || event.id.split(':decision:')[0] || '';
|
||||
const approval = approvalRows.get(approvalId);
|
||||
return {
|
||||
history_id: event.id,
|
||||
event_id: event.id,
|
||||
approval_id: approvalId || null,
|
||||
action: 'decision',
|
||||
status: stringValue(payload.nextStatus ?? payload.next_status ?? payload.decision) || null,
|
||||
actor: stringValue(payload.actor) || stringValue(payload.source) || 'qimingclaw-approval-decision',
|
||||
risk_level: stringValue(approval?.risk_level) || null,
|
||||
due_at: stringValue(approval?.due_at) || null,
|
||||
delegate_to: stringValue(approval?.delegate_to) || null,
|
||||
comment_length: null,
|
||||
message: event.message || null,
|
||||
plan_id: event.planId || stringValue(payload.planId ?? payload.plan_id) || stringValue(approval?.plan_id) || null,
|
||||
task_id: event.taskId || stringValue(payload.taskId ?? payload.task_id) || stringValue(approval?.task_id) || null,
|
||||
run_id: event.runId || stringValue(payload.runId ?? payload.run_id) || stringValue(approval?.run_id) || null,
|
||||
occurred_at: event.occurredAt,
|
||||
updated_at: event.occurredAt,
|
||||
sync_status: event.syncStatus ?? null,
|
||||
entity_type: 'approval_history',
|
||||
entity_id: event.id,
|
||||
source: BUSINESS_VIEW_SOURCE,
|
||||
};
|
||||
});
|
||||
return [...actionRows, ...decisionRows]
|
||||
.sort((left, right) => stringValue(right.occurred_at).localeCompare(stringValue(left.occurred_at)) || stringValue(right.history_id).localeCompare(stringValue(left.history_id)));
|
||||
}
|
||||
|
||||
function filterApprovalHistoryRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] {
|
||||
const approvalId = stringValue(searchParams.get('approval_id')) || stringValue(searchParams.get('id'));
|
||||
const action = stringValue(searchParams.get('action'));
|
||||
const riskLevel = stringValue(searchParams.get('risk_level'));
|
||||
return filterBusinessRows(rows, searchParams)
|
||||
.filter((row) => !approvalId || stringValue(row.approval_id) === approvalId || stringValue(row.history_id) === approvalId || stringValue(row.event_id) === approvalId)
|
||||
.filter((row) => !action || stringValue(row.action) === action)
|
||||
.filter((row) => !riskLevel || stringValue(row.risk_level) === riskLevel);
|
||||
}
|
||||
|
||||
function approvalActionSummary(
|
||||
snapshot: QimingclawSnapshot | null,
|
||||
approvalId: string,
|
||||
@@ -3230,6 +3324,7 @@ function businessRowEntityIds(row: BusinessViewRow): string[] {
|
||||
row.task_id,
|
||||
row.run_id,
|
||||
row.event_id,
|
||||
row.history_id,
|
||||
row.artifact_id,
|
||||
row.approval_id,
|
||||
row.notification_id,
|
||||
@@ -3547,6 +3642,35 @@ function defaultNotificationPreferences(): NotificationPreferences {
|
||||
};
|
||||
}
|
||||
|
||||
function defaultApprovalSubscriptionPreferences(): ApprovalSubscriptionPreferences {
|
||||
return {
|
||||
enabled: true,
|
||||
subscribed_actions: [...APPROVAL_SUBSCRIPTION_ACTIONS],
|
||||
subscribed_risk_levels: ['high', 'critical'],
|
||||
notify_on_timeout: true,
|
||||
updated_at: null,
|
||||
};
|
||||
}
|
||||
|
||||
function approvalSubscriptionPreferences(state: Partial<AdapterState> | null | undefined): ApprovalSubscriptionPreferences {
|
||||
const stored = state?.approvalSubscriptionPreferences;
|
||||
const fallback = defaultApprovalSubscriptionPreferences();
|
||||
if (!stored || typeof stored !== 'object') return fallback;
|
||||
const actions = Array.isArray(stored.subscribed_actions)
|
||||
? stored.subscribed_actions.filter((action): action is ApprovalSubscriptionPreferences['subscribed_actions'][number] => APPROVAL_SUBSCRIPTION_ACTIONS.includes(action as ApprovalSubscriptionPreferences['subscribed_actions'][number]))
|
||||
: fallback.subscribed_actions;
|
||||
const riskLevels = Array.isArray(stored.subscribed_risk_levels)
|
||||
? stored.subscribed_risk_levels.filter((level): level is ApprovalSubscriptionPreferences['subscribed_risk_levels'][number] => APPROVAL_SUBSCRIPTION_RISK_LEVELS.includes(level as ApprovalSubscriptionPreferences['subscribed_risk_levels'][number]))
|
||||
: fallback.subscribed_risk_levels;
|
||||
return {
|
||||
enabled: stored.enabled !== false,
|
||||
subscribed_actions: [...new Set(actions)],
|
||||
subscribed_risk_levels: [...new Set(riskLevels)],
|
||||
notify_on_timeout: stored.notify_on_timeout !== false,
|
||||
updated_at: typeof stored.updated_at === 'string' ? stored.updated_at : fallback.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
function notificationPreferences(state: Partial<AdapterState> | null | undefined): NotificationPreferences {
|
||||
const stored = state?.notificationPreferences;
|
||||
const fallback = defaultNotificationPreferences();
|
||||
@@ -3600,6 +3724,31 @@ async function handleNotificationPreferencesPatch(
|
||||
return next;
|
||||
}
|
||||
|
||||
async function handleApprovalSubscriptionPreferencesPatch(
|
||||
patch: Partial<ApprovalSubscriptionPreferences>,
|
||||
snapshot: QimingclawSnapshot | null,
|
||||
): Promise<ApprovalSubscriptionPreferences> {
|
||||
const state = snapshot?.uiState ?? await readBridgeUiState() ?? readState();
|
||||
const current = approvalSubscriptionPreferences(state);
|
||||
const next = approvalSubscriptionPreferences({
|
||||
approvalSubscriptionPreferences: {
|
||||
...current,
|
||||
...patch,
|
||||
updated_at: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
await saveState(
|
||||
{
|
||||
...normalizeAdapterState(state),
|
||||
approvalSubscriptionPreferences: next,
|
||||
},
|
||||
'update_approval_subscriptions',
|
||||
undefined,
|
||||
{ approval_subscriptions: next },
|
||||
);
|
||||
return next;
|
||||
}
|
||||
|
||||
function approvalNotifications(snapshot: QimingclawSnapshot | null): UserNotification[] {
|
||||
const summaries = new Map(businessApprovalRows(snapshot).map((approval) => [stringValue(approval.approval_id), approval]));
|
||||
return buildApprovals(snapshot)
|
||||
|
||||
Reference in New Issue
Block a user