吸收数字员工管理端通知视图
This commit is contained in:
@@ -687,7 +687,7 @@ const PLAN_SCHEDULES: Record<string, { expression: string; nextRun: string; sche
|
|||||||
|
|
||||||
const BUSINESS_VIEW_SOURCE = 'qimingclaw-runtime-projection';
|
const BUSINESS_VIEW_SOURCE = 'qimingclaw-runtime-projection';
|
||||||
|
|
||||||
type BusinessViewKind = 'plans' | 'tasks' | 'runs' | 'events' | 'artifacts' | 'approvals' | 'route-decisions' | 'audits';
|
type BusinessViewKind = 'plans' | 'tasks' | 'runs' | 'events' | 'artifacts' | 'approvals' | 'route-decisions' | 'notifications' | 'audits';
|
||||||
type BusinessViewRow = Record<string, unknown>;
|
type BusinessViewRow = Record<string, unknown>;
|
||||||
|
|
||||||
export function isQimingclawDigitalApiEnabled(): boolean {
|
export function isQimingclawDigitalApiEnabled(): boolean {
|
||||||
@@ -771,7 +771,7 @@ export async function handleQimingclawDigitalApi<T>(
|
|||||||
const approvalId = decodeURIComponent(approvalActionMatch[1] || '');
|
const approvalId = decodeURIComponent(approvalActionMatch[1] || '');
|
||||||
return await handleApprovalAction(approvalId, parseOptionalJsonBody(options.body), await readQimingclawSnapshot()) as T;
|
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|audits)$/);
|
const businessViewMatch = pathname.match(/^\/api\/digital-employee\/(plans|tasks|runs|events|artifacts|approvals|route-decisions|notifications|audits)$/);
|
||||||
if (method === 'GET' && businessViewMatch) {
|
if (method === 'GET' && businessViewMatch) {
|
||||||
return buildBusinessViewResponse(businessViewMatch[1] as BusinessViewKind, await readQimingclawSnapshot(), url.searchParams) as T;
|
return buildBusinessViewResponse(businessViewMatch[1] as BusinessViewKind, await readQimingclawSnapshot(), url.searchParams) as T;
|
||||||
}
|
}
|
||||||
@@ -2507,13 +2507,16 @@ function buildBusinessViewResponse(kind: BusinessViewKind, snapshot: QimingclawS
|
|||||||
artifacts: businessArtifactRows(snapshot),
|
artifacts: businessArtifactRows(snapshot),
|
||||||
approvals: businessApprovalRows(snapshot),
|
approvals: businessApprovalRows(snapshot),
|
||||||
'route-decisions': routeDecisionRows(snapshot),
|
'route-decisions': routeDecisionRows(snapshot),
|
||||||
|
notifications: notificationRows(snapshot, notificationAdapterState(snapshot)),
|
||||||
audits: auditRows(snapshot),
|
audits: auditRows(snapshot),
|
||||||
};
|
};
|
||||||
const rows = kind === 'route-decisions'
|
const rows = kind === 'route-decisions'
|
||||||
? filterRouteDecisionRows(rowsByKind[kind], searchParams)
|
? filterRouteDecisionRows(rowsByKind[kind], searchParams)
|
||||||
: kind === 'audits'
|
: kind === 'notifications'
|
||||||
? filterAuditRows(rowsByKind[kind], searchParams)
|
? filterNotificationRows(rowsByKind[kind], searchParams)
|
||||||
: filterBusinessRows(rowsByKind[kind], searchParams);
|
: kind === 'audits'
|
||||||
|
? filterAuditRows(rowsByKind[kind], searchParams)
|
||||||
|
: filterBusinessRows(rowsByKind[kind], searchParams);
|
||||||
return {
|
return {
|
||||||
...paginateBusinessRows(rows, searchParams),
|
...paginateBusinessRows(rows, searchParams),
|
||||||
source: BUSINESS_VIEW_SOURCE,
|
source: BUSINESS_VIEW_SOURCE,
|
||||||
@@ -2555,6 +2558,50 @@ function filterRouteDecisionRows(rows: BusinessViewRow[], searchParams: URLSearc
|
|||||||
.filter((row) => !policyResult || stringValue(row.policy_result) === policyResult);
|
.filter((row) => !policyResult || stringValue(row.policy_result) === policyResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function notificationRows(snapshot: QimingclawSnapshot | null, state: AdapterState): BusinessViewRow[] {
|
||||||
|
const preferences = notificationPreferences(state);
|
||||||
|
return buildNotificationCandidates(snapshot, state)
|
||||||
|
.map((notification) => ({
|
||||||
|
notification_id: notification.notification_id,
|
||||||
|
kind: notification.kind,
|
||||||
|
level: notification.level,
|
||||||
|
status: notification.status,
|
||||||
|
title: notification.title,
|
||||||
|
body: notification.body,
|
||||||
|
plan_id: notification.plan_id,
|
||||||
|
task_id: notification.task_id,
|
||||||
|
correlation_id: notification.correlation_id,
|
||||||
|
created_at: notification.created_at,
|
||||||
|
updated_at: notification.replied_at || notification.dismissed_at || notification.read_at || notification.created_at,
|
||||||
|
read_at: notification.read_at ?? null,
|
||||||
|
dismissed_at: notification.dismissed_at ?? null,
|
||||||
|
replied_at: notification.replied_at ?? null,
|
||||||
|
muted_by_preferences: !notificationMatchesPreferences(notification, preferences),
|
||||||
|
entity_type: 'notification',
|
||||||
|
entity_id: notification.notification_id,
|
||||||
|
source: BUSINESS_VIEW_SOURCE,
|
||||||
|
}))
|
||||||
|
.sort((left, right) => stringValue(right.created_at).localeCompare(stringValue(left.created_at)) || stringValue(right.notification_id).localeCompare(stringValue(left.notification_id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterNotificationRows(rows: BusinessViewRow[], searchParams: URLSearchParams): BusinessViewRow[] {
|
||||||
|
const id = stringValue(searchParams.get('id'));
|
||||||
|
const kind = stringValue(searchParams.get('kind'));
|
||||||
|
const level = stringValue(searchParams.get('level'));
|
||||||
|
const muted = stringValue(searchParams.get('muted')).toLowerCase();
|
||||||
|
return filterBusinessRows(rows, searchParams)
|
||||||
|
.filter((row) => !id || stringValue(row.notification_id) === id || stringValue(row.correlation_id) === id || stringValue(row.entity_id) === id)
|
||||||
|
.filter((row) => !kind || stringValue(row.kind) === kind)
|
||||||
|
.filter((row) => !level || stringValue(row.level) === level)
|
||||||
|
.filter((row) => {
|
||||||
|
if (!muted) return true;
|
||||||
|
const mutedValue = row.muted_by_preferences === true;
|
||||||
|
if (['1', 'true', 'yes'].includes(muted)) return mutedValue;
|
||||||
|
if (['0', 'false', 'no'].includes(muted)) return !mutedValue;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function routeDecisionFromRuntimePayload(payload: Record<string, unknown>, event: QimingclawEventRecord): RouteDecisionRecord {
|
function routeDecisionFromRuntimePayload(payload: Record<string, unknown>, event: QimingclawEventRecord): RouteDecisionRecord {
|
||||||
const decision = asRecord(payload.routeDecision ?? payload.route_decision ?? payload) ?? {};
|
const decision = asRecord(payload.routeDecision ?? payload.route_decision ?? payload) ?? {};
|
||||||
return {
|
return {
|
||||||
@@ -3185,6 +3232,7 @@ function businessRowEntityIds(row: BusinessViewRow): string[] {
|
|||||||
row.event_id,
|
row.event_id,
|
||||||
row.artifact_id,
|
row.artifact_id,
|
||||||
row.approval_id,
|
row.approval_id,
|
||||||
|
row.notification_id,
|
||||||
row.report_id,
|
row.report_id,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -3468,6 +3516,11 @@ function buildNotificationsResponse(status: string | null, snapshot: QimingclawS
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildNotifications(snapshot: QimingclawSnapshot | null, state: AdapterState): UserNotification[] {
|
function buildNotifications(snapshot: QimingclawSnapshot | null, state: AdapterState): UserNotification[] {
|
||||||
|
return buildNotificationCandidates(snapshot, state)
|
||||||
|
.filter((notification) => notificationMatchesPreferences(notification, state.notificationPreferences));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildNotificationCandidates(snapshot: QimingclawSnapshot | null, state: AdapterState): UserNotification[] {
|
||||||
const notifications = dedupeNotifications([
|
const notifications = dedupeNotifications([
|
||||||
...approvalNotifications(snapshot),
|
...approvalNotifications(snapshot),
|
||||||
...approvalActionNotifications(snapshot),
|
...approvalActionNotifications(snapshot),
|
||||||
@@ -3482,7 +3535,6 @@ function buildNotifications(snapshot: QimingclawSnapshot | null, state: AdapterS
|
|||||||
const overlays = state.notificationStateById ?? {};
|
const overlays = state.notificationStateById ?? {};
|
||||||
return notifications
|
return notifications
|
||||||
.map((notification) => applyNotificationOverlay(notification, overlays[notification.notification_id]))
|
.map((notification) => applyNotificationOverlay(notification, overlays[notification.notification_id]))
|
||||||
.filter((notification) => notificationMatchesPreferences(notification, state.notificationPreferences))
|
|
||||||
.sort((left, right) => right.created_at.localeCompare(left.created_at) || right.notification_id.localeCompare(left.notification_id));
|
.sort((left, right) => right.created_at.localeCompare(left.created_at) || right.notification_id.localeCompare(left.notification_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -16,7 +16,7 @@
|
|||||||
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
|
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script type="module" crossorigin src="./assets/index-DDRkk0GO.js"></script>
|
<script type="module" crossorigin src="./assets/index-CP7OsYRU.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-DqdYYbRq.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-DqdYYbRq.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -202,13 +202,19 @@ function checkDigitalNotificationPreferences() {
|
|||||||
const typesPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "types", "api.ts");
|
const typesPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "types", "api.ts");
|
||||||
const shellPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "pages", "digital", "DigitalEmployeeShell.tsx");
|
const shellPath = path.join(packageRoot, "embedded", "sgrobot-digital", "src", "pages", "digital", "DigitalEmployeeShell.tsx");
|
||||||
const stateServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "stateService.ts");
|
const stateServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "stateService.ts");
|
||||||
|
const syncServicePath = path.join(packageRoot, "src", "main", "services", "digitalEmployee", "syncService.ts");
|
||||||
const adapter = fs.readFileSync(adapterPath, "utf8");
|
const adapter = fs.readFileSync(adapterPath, "utf8");
|
||||||
const api = fs.readFileSync(apiPath, "utf8");
|
const api = fs.readFileSync(apiPath, "utf8");
|
||||||
const types = fs.readFileSync(typesPath, "utf8");
|
const types = fs.readFileSync(typesPath, "utf8");
|
||||||
const shell = fs.readFileSync(shellPath, "utf8");
|
const shell = fs.readFileSync(shellPath, "utf8");
|
||||||
const stateService = fs.readFileSync(stateServicePath, "utf8");
|
const stateService = fs.readFileSync(stateServicePath, "utf8");
|
||||||
|
const syncService = fs.readFileSync(syncServicePath, "utf8");
|
||||||
const requiredSnippets = [
|
const requiredSnippets = [
|
||||||
[adapter, "/api/notifications/preferences", "adapter notification preference endpoint"],
|
[adapter, "/api/notifications/preferences", "adapter notification preference endpoint"],
|
||||||
|
[adapter, "route-decisions|notifications|audits", "management notification business route"],
|
||||||
|
[adapter, "notificationRows", "management notification rows"],
|
||||||
|
[adapter, "filterNotificationRows", "management notification filters"],
|
||||||
|
[adapter, "muted_by_preferences", "management notification muted marker"],
|
||||||
[adapter, "handleNotificationPreferencesPatch", "adapter notification preference patch handler"],
|
[adapter, "handleNotificationPreferencesPatch", "adapter notification preference patch handler"],
|
||||||
[adapter, "notificationMatchesPreferences", "adapter notification preference filter"],
|
[adapter, "notificationMatchesPreferences", "adapter notification preference filter"],
|
||||||
[adapter, "update_notification_preferences", "adapter notification preference save action"],
|
[adapter, "update_notification_preferences", "adapter notification preference save action"],
|
||||||
@@ -218,6 +224,9 @@ function checkDigitalNotificationPreferences() {
|
|||||||
[shell, "de-notification-preferences", "notification preferences UI"],
|
[shell, "de-notification-preferences", "notification preferences UI"],
|
||||||
[shell, "NOTIFICATION_KIND_OPTIONS", "notification kind mute UI options"],
|
[shell, "NOTIFICATION_KIND_OPTIONS", "notification kind mute UI options"],
|
||||||
[stateService, "update_notification_preferences", "notification preference runtime event"],
|
[stateService, "update_notification_preferences", "notification preference runtime event"],
|
||||||
|
[syncService, "notificationBusinessView", "notification sync business view"],
|
||||||
|
[syncService, "isNotificationEvent", "notification event classifier"],
|
||||||
|
[syncService, "return \"notification\"", "notification business category"],
|
||||||
];
|
];
|
||||||
const missing = requiredSnippets
|
const missing = requiredSnippets
|
||||||
.filter(([content, snippet]) => !content.includes(snippet))
|
.filter(([content, snippet]) => !content.includes(snippet))
|
||||||
|
|||||||
@@ -533,6 +533,68 @@ describe("digital employee sync service", () => {
|
|||||||
status: "failed",
|
status: "failed",
|
||||||
reason: "computer chat failed",
|
reason: "computer chat failed",
|
||||||
});
|
});
|
||||||
|
insertEventEntity("event-notification-read", "digital_workday_notification_read", {
|
||||||
|
metadata: {
|
||||||
|
notification_id: "task:task-1",
|
||||||
|
action: "read",
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
notificationStateById: {
|
||||||
|
"task:task-1": {
|
||||||
|
status: "read",
|
||||||
|
read_at: "2026-06-07T08:01:00.000Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
insertEventEntity("event-notification-dismiss", "digital_workday_notification_dismiss", {
|
||||||
|
metadata: {
|
||||||
|
notification_id: "task:task-2",
|
||||||
|
action: "dismiss",
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
notificationStateById: {
|
||||||
|
"task:task-2": {
|
||||||
|
status: "dismissed",
|
||||||
|
dismissed_at: "2026-06-07T08:02:00.000Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
insertEventEntity("event-notification-reply", "digital_workday_notification_reply", {
|
||||||
|
metadata: {
|
||||||
|
notification_id: "input:task-3",
|
||||||
|
action: "reply",
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
notificationStateById: {
|
||||||
|
"input:task-3": {
|
||||||
|
status: "read",
|
||||||
|
read_at: "2026-06-07T08:03:00.000Z",
|
||||||
|
reply: "请改用客户备用手机号",
|
||||||
|
replied_at: "2026-06-07T08:03:30.000Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
insertEventEntity("event-notification-preferences", "digital_workday_update_notification_preferences", {
|
||||||
|
metadata: {
|
||||||
|
notification_preferences: {
|
||||||
|
enabled: false,
|
||||||
|
muted_kinds: ["task_finished", "need_input"],
|
||||||
|
muted_levels: ["info"],
|
||||||
|
updated_at: "2026-06-07T08:04:00.000Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
notificationPreferences: {
|
||||||
|
enabled: false,
|
||||||
|
muted_kinds: ["task_finished", "need_input"],
|
||||||
|
muted_levels: ["info"],
|
||||||
|
updated_at: "2026-06-07T08:04:00.000Z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
mockState.fetch.mockResolvedValue(
|
mockState.fetch.mockResolvedValue(
|
||||||
jsonResponse({
|
jsonResponse({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -542,6 +604,10 @@ describe("digital employee sync service", () => {
|
|||||||
{ entity_type: "event", entity_id: "event-artifact" },
|
{ entity_type: "event", entity_id: "event-artifact" },
|
||||||
{ entity_type: "event", entity_id: "event-skill" },
|
{ entity_type: "event", entity_id: "event-skill" },
|
||||||
{ entity_type: "event", entity_id: "event-dispatch" },
|
{ entity_type: "event", entity_id: "event-dispatch" },
|
||||||
|
{ entity_type: "event", entity_id: "event-notification-read" },
|
||||||
|
{ entity_type: "event", entity_id: "event-notification-dismiss" },
|
||||||
|
{ entity_type: "event", entity_id: "event-notification-reply" },
|
||||||
|
{ entity_type: "event", entity_id: "event-notification-preferences" },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -602,6 +668,36 @@ describe("digital employee sync service", () => {
|
|||||||
status: "failed",
|
status: "failed",
|
||||||
reason: "computer chat failed",
|
reason: "computer chat failed",
|
||||||
});
|
});
|
||||||
|
expect(businessView("event-notification-read")).toMatchObject({
|
||||||
|
category: "notification",
|
||||||
|
notification_id: "task:task-1",
|
||||||
|
action: "read",
|
||||||
|
status: "read",
|
||||||
|
read_at: "2026-06-07T08:01:00.000Z",
|
||||||
|
});
|
||||||
|
expect(businessView("event-notification-dismiss")).toMatchObject({
|
||||||
|
category: "notification",
|
||||||
|
notification_id: "task:task-2",
|
||||||
|
action: "dismiss",
|
||||||
|
status: "dismissed",
|
||||||
|
dismissed_at: "2026-06-07T08:02:00.000Z",
|
||||||
|
});
|
||||||
|
expect(businessView("event-notification-reply")).toMatchObject({
|
||||||
|
category: "notification",
|
||||||
|
notification_id: "input:task-3",
|
||||||
|
action: "reply",
|
||||||
|
status: "read",
|
||||||
|
read_at: "2026-06-07T08:03:00.000Z",
|
||||||
|
replied_at: "2026-06-07T08:03:30.000Z",
|
||||||
|
reply_length: 10,
|
||||||
|
});
|
||||||
|
expect(businessView("event-notification-reply")).not.toHaveProperty("reply");
|
||||||
|
expect(businessView("event-notification-preferences")).toMatchObject({
|
||||||
|
category: "notification",
|
||||||
|
preferences_enabled: false,
|
||||||
|
muted_kinds: ["task_finished", "need_input"],
|
||||||
|
muted_levels: ["info"],
|
||||||
|
});
|
||||||
expect(readEntity("event", "event-route")).toMatchObject({
|
expect(readEntity("event", "event-route")).toMatchObject({
|
||||||
sync_status: "synced",
|
sync_status: "synced",
|
||||||
sync_error: null,
|
sync_error: null,
|
||||||
|
|||||||
@@ -568,6 +568,7 @@ function eventSpecificBusinessView(
|
|||||||
}
|
}
|
||||||
if (kind.startsWith("skill_call_")) return skillCallBusinessView(payload);
|
if (kind.startsWith("skill_call_")) return skillCallBusinessView(payload);
|
||||||
if (kind.startsWith("plan_step_dispatch_")) return planStepDispatchBusinessView(payload);
|
if (kind.startsWith("plan_step_dispatch_")) return planStepDispatchBusinessView(payload);
|
||||||
|
if (isNotificationEvent(kind)) return notificationBusinessView(payload);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,16 +638,46 @@ function planStepDispatchBusinessView(payload: Record<string, unknown>): Record<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function notificationBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
|
||||||
|
const metadata = objectRecord(payload.metadata) ?? {};
|
||||||
|
const state = objectRecord(payload.state) ?? {};
|
||||||
|
const preferences = objectRecord(metadata.notification_preferences) ?? objectRecord(state.notificationPreferences) ?? {};
|
||||||
|
const overlays = objectRecord(state.notificationStateById) ?? {};
|
||||||
|
const notificationId = stringField(metadata.notification_id ?? payload.notification_id ?? payload.notificationId) || null;
|
||||||
|
const overlay = notificationId ? objectRecord(overlays[notificationId]) ?? {} : {};
|
||||||
|
const reply = stringField(overlay.reply ?? metadata.reply ?? payload.reply);
|
||||||
|
return {
|
||||||
|
notification_id: notificationId,
|
||||||
|
action: stringField(metadata.action ?? payload.action) || null,
|
||||||
|
status: stringField(overlay.status ?? payload.status) || null,
|
||||||
|
read_at: stringField(overlay.read_at ?? payload.read_at ?? payload.readAt) || null,
|
||||||
|
dismissed_at: stringField(overlay.dismissed_at ?? payload.dismissed_at ?? payload.dismissedAt) || null,
|
||||||
|
replied_at: stringField(overlay.replied_at ?? payload.replied_at ?? payload.repliedAt) || null,
|
||||||
|
reply_length: reply ? reply.length : null,
|
||||||
|
preferences_enabled: typeof preferences.enabled === "boolean" ? preferences.enabled : null,
|
||||||
|
muted_kinds: stringArrayField(preferences.muted_kinds ?? preferences.mutedKinds),
|
||||||
|
muted_levels: stringArrayField(preferences.muted_levels ?? preferences.mutedLevels),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function eventBusinessCategory(kind: string): string {
|
function eventBusinessCategory(kind: string): string {
|
||||||
if (kind === "route_decision") return "route";
|
if (kind === "route_decision") return "route";
|
||||||
if (kind.startsWith("approval_")) return "approval";
|
if (kind.startsWith("approval_")) return "approval";
|
||||||
if (kind.startsWith("artifact_")) return "artifact";
|
if (kind.startsWith("artifact_")) return "artifact";
|
||||||
if (kind.startsWith("skill_call_")) return "skill";
|
if (kind.startsWith("skill_call_")) return "skill";
|
||||||
if (kind.startsWith("plan_step_dispatch_")) return "dispatch";
|
if (kind.startsWith("plan_step_dispatch_")) return "dispatch";
|
||||||
|
if (isNotificationEvent(kind)) return "notification";
|
||||||
if (kind.startsWith("governance_")) return "governance";
|
if (kind.startsWith("governance_")) return "governance";
|
||||||
return "runtime";
|
return "runtime";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isNotificationEvent(kind: string): boolean {
|
||||||
|
return kind === "digital_workday_update_notification_preferences"
|
||||||
|
|| kind === "digital_workday_notification_read"
|
||||||
|
|| kind === "digital_workday_notification_dismiss"
|
||||||
|
|| kind === "digital_workday_notification_reply";
|
||||||
|
}
|
||||||
|
|
||||||
function routeDecisionAttentionLevel(policyResult: string, reasonCodes: string[], createdCommandId: string | null): "info" | "action" | "warn" | "error" {
|
function routeDecisionAttentionLevel(policyResult: string, reasonCodes: string[], createdCommandId: string | null): "info" | "action" | "warn" | "error" {
|
||||||
if (policyResult && policyResult !== "accepted") return "error";
|
if (policyResult && policyResult !== "accepted") return "error";
|
||||||
if (reasonCodes.includes("candidate_skills_disabled") || reasonCodes.includes("route_action_missing_context")) return "warn";
|
if (reasonCodes.includes("candidate_skills_disabled") || reasonCodes.includes("route_action_missing_context")) return "warn";
|
||||||
|
|||||||
@@ -583,9 +583,9 @@ GET /api/digital-employee/approvals
|
|||||||
- 建议:下一轮先补管理端审批历史与通知订阅策略,再做多步审批和风险策略联动。
|
- 建议:下一轮先补管理端审批历史与通知订阅策略,再做多步审批和风险策略联动。
|
||||||
|
|
||||||
9. **通知 / Inbox / 用户消息**
|
9. **通知 / Inbox / 用户消息**
|
||||||
- 已吸收:运行事件和同步失败能在数字员工页面展示;embedded adapter 已接管 sgRobot 风格 `/api/notifications`、未读数、read/dismiss/reply,本地 UI state 会保存通知已读、忽略和回复 overlay;顶部 Bell 已开始展示 approval、同步失败、服务异常和任务完成通知;本地通知订阅偏好已接入 `/api/notifications/preferences`,可按总开关、等级和常用类型过滤 Inbox,通知投影与本地订阅策略已开始闭环。
|
- 已吸收:运行事件和同步失败能在数字员工页面展示;embedded adapter 已接管 sgRobot 风格 `/api/notifications`、未读数、read/dismiss/reply,本地 UI state 会保存通知已读、忽略和回复 overlay;顶部 Bell 已开始展示 approval、同步失败、服务异常和任务完成通知;本地通知订阅偏好已接入 `/api/notifications/preferences`,可按总开关、等级和常用类型过滤 Inbox;`/api/digital-employee/notifications` 已提供管理端可消费的本地通知业务视图,通知动作和订阅偏好事件的 sync `business_view` 会归类为 notification 并提炼状态字段,通知投影、本地订阅策略与管理端消费模型已开始闭环。
|
||||||
- 缺口:仍缺少系统级桌面通知、管理端通知视图、跨端未读同步和任务输入自动转化。
|
- 缺口:仍缺少系统级桌面通知、真实管理端通知页面、跨端未读拉取回写和任务输入自动转化。
|
||||||
- 建议:下一轮围绕管理端通知视图和跨端订阅同步,把通知从本地 Inbox 投影推进到跨端协同入口。
|
- 建议:下一轮围绕跨端未读同步和任务输入自动转化,把本地通知业务视图推进到真正跨端协同入口。
|
||||||
|
|
||||||
10. **入口路由 / 任务分流(路由业务视图与通知联动已开始)**
|
10. **入口路由 / 任务分流(路由业务视图与通知联动已开始)**
|
||||||
- 已吸收:管理端通过 `/computer/chat` 进入 qimingclaw 后会生成本地任务事实;embedded `/api/ingress/route`、`/api/route-decisions` 和 `/api/digital-employee/route-decisions` 已由 qimingclaw adapter 接管。
|
- 已吸收:管理端通过 `/computer/chat` 进入 qimingclaw 后会生成本地任务事实;embedded `/api/ingress/route`、`/api/route-decisions` 和 `/api/digital-employee/route-decisions` 已由 qimingclaw adapter 接管。
|
||||||
|
|||||||
Reference in New Issue
Block a user