吸收数字员工通知回复任务输入
This commit is contained in:
@@ -3819,11 +3819,14 @@ async function handleNotificationAction(
|
||||
action: 'read' | 'dismiss' | 'reply',
|
||||
body: Record<string, unknown>,
|
||||
snapshot: QimingclawSnapshot | null,
|
||||
): Promise<{ ok: boolean; task_id?: string; status?: string }> {
|
||||
): Promise<{ ok: boolean; task_id?: string; status?: string; task_input_recorded?: boolean; governance_command_id?: string; governance_event_id?: string }> {
|
||||
const state = snapshot?.uiState ?? await readBridgeUiState() ?? readState();
|
||||
const current = buildNotifications(snapshot, state).find((notification) => notification.notification_id === notificationId);
|
||||
const now = new Date().toISOString();
|
||||
const overlay: NotificationState = { ...(state.notificationStateById?.[notificationId] ?? {}) };
|
||||
const previousOverlay = state.notificationStateById?.[notificationId];
|
||||
const alreadyReplied = Boolean(previousOverlay?.replied_at);
|
||||
const overlay: NotificationState = { ...(previousOverlay ?? {}) };
|
||||
let replyContent = '';
|
||||
if (action === 'read') {
|
||||
overlay.status = current?.status === 'resolved' ? 'resolved' : 'read';
|
||||
overlay.read_at = overlay.read_at ?? now;
|
||||
@@ -3833,7 +3836,8 @@ async function handleNotificationAction(
|
||||
} else {
|
||||
overlay.status = current?.status === 'resolved' ? 'resolved' : 'read';
|
||||
overlay.read_at = overlay.read_at ?? now;
|
||||
overlay.reply = stringValue(body.content ?? body.reply ?? body.message);
|
||||
replyContent = stringValue(body.content ?? body.reply ?? body.message);
|
||||
overlay.reply = replyContent;
|
||||
overlay.replied_at = now;
|
||||
}
|
||||
const nextState: AdapterState = {
|
||||
@@ -3844,7 +3848,51 @@ async function handleNotificationAction(
|
||||
},
|
||||
};
|
||||
await saveState(nextState, `notification_${action}`, undefined, { notification_id: notificationId, action });
|
||||
return { ok: true, task_id: current?.task_id, status: overlay.status };
|
||||
const taskInput = action === 'reply' && current?.kind === 'need_input' && current.task_id && replyContent && !alreadyReplied
|
||||
? await recordNotificationTaskInput(current, replyContent, snapshot)
|
||||
: null;
|
||||
return {
|
||||
ok: true,
|
||||
task_id: current?.task_id,
|
||||
status: overlay.status,
|
||||
task_input_recorded: Boolean(taskInput?.accepted),
|
||||
governance_command_id: taskInput?.command_id,
|
||||
governance_event_id: governanceEventIdFromCommand(taskInput),
|
||||
};
|
||||
}
|
||||
|
||||
async function recordNotificationTaskInput(
|
||||
notification: UserNotification,
|
||||
replyContent: string,
|
||||
snapshot: QimingclawSnapshot | null,
|
||||
): Promise<GovernanceCommandResponse> {
|
||||
const stamp = Date.now();
|
||||
return handleGovernanceCommand({
|
||||
command_kind: 'provide_task_input',
|
||||
context_refs: {
|
||||
plan_id: notification.plan_id || undefined,
|
||||
task_id: notification.task_id,
|
||||
},
|
||||
payload: {
|
||||
input: replyContent,
|
||||
source: 'notification_reply',
|
||||
notification_id: notification.notification_id,
|
||||
plan_id: notification.plan_id || undefined,
|
||||
task_id: notification.task_id,
|
||||
},
|
||||
correlation_id: `qimingclaw-notification-reply-${slugifyIdPart(notification.notification_id)}-${stamp}`,
|
||||
idempotency_key: `qimingclaw-notification-reply-${notification.notification_id}`,
|
||||
}, snapshot);
|
||||
}
|
||||
|
||||
function governanceEventIdFromCommand(response: GovernanceCommandResponse | null): string | undefined {
|
||||
const runId = stringFromUnknown(response?.result?.run_id);
|
||||
if (!response?.accepted || !runId || !response.command_id) return undefined;
|
||||
return `${runId}:provide_task_input:${safeEventIdPart(response.command_id)}`;
|
||||
}
|
||||
|
||||
function safeEventIdPart(value: string): string {
|
||||
return value.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'unknown';
|
||||
}
|
||||
|
||||
function buildWorkdayDecisions(
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -16,7 +16,7 @@
|
||||
console.warn("[qimingclaw] digital employee auth bootstrap skipped", error);
|
||||
}
|
||||
</script>
|
||||
<script type="module" crossorigin src="./assets/index-CP7OsYRU.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-AFoy4EL3.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-DqdYYbRq.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -215,6 +215,10 @@ function checkDigitalNotificationPreferences() {
|
||||
[adapter, "notificationRows", "management notification rows"],
|
||||
[adapter, "filterNotificationRows", "management notification filters"],
|
||||
[adapter, "muted_by_preferences", "management notification muted marker"],
|
||||
[adapter, "recordNotificationTaskInput", "notification reply task input bridge"],
|
||||
[adapter, "notification_reply", "notification reply task input source"],
|
||||
[adapter, "task_input_recorded", "notification reply task input result"],
|
||||
[adapter, "provide_task_input", "notification reply governance command"],
|
||||
[adapter, "handleNotificationPreferencesPatch", "adapter notification preference patch handler"],
|
||||
[adapter, "notificationMatchesPreferences", "adapter notification preference filter"],
|
||||
[adapter, "update_notification_preferences", "adapter notification preference save action"],
|
||||
@@ -225,6 +229,9 @@ function checkDigitalNotificationPreferences() {
|
||||
[shell, "NOTIFICATION_KIND_OPTIONS", "notification kind mute UI options"],
|
||||
[stateService, "update_notification_preferences", "notification preference runtime event"],
|
||||
[syncService, "notificationBusinessView", "notification sync business view"],
|
||||
[syncService, "governanceProvideTaskInputBusinessView", "notification reply task input sync view"],
|
||||
[syncService, "governance_provide_task_input", "task input sync event kind"],
|
||||
[syncService, "input_length", "task input sync redacted length"],
|
||||
[syncService, "isNotificationEvent", "notification event classifier"],
|
||||
[syncService, "return \"notification\"", "notification business category"],
|
||||
];
|
||||
|
||||
@@ -1517,14 +1517,22 @@ describe("digital employee state service", () => {
|
||||
runId: "run-governance",
|
||||
stepId: "step-governance",
|
||||
accepted: true,
|
||||
payload: { input: "继续使用最新客户名单" },
|
||||
payload: {
|
||||
input: "继续使用最新客户名单",
|
||||
source: "notification_reply",
|
||||
notification_id: "input:task-governance",
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockState.db?.tasks.get("task-governance")).toMatchObject({ status: "paused" });
|
||||
expect(JSON.parse(mockState.db?.tasks.get("task-governance")?.payload ?? "{}")).toMatchObject({
|
||||
lastGovernanceCommand: {
|
||||
commandKind: "provide_task_input",
|
||||
payload: { input: "继续使用最新客户名单" },
|
||||
payload: {
|
||||
input: "继续使用最新客户名单",
|
||||
source: "notification_reply",
|
||||
notification_id: "input:task-governance",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1545,6 +1553,10 @@ describe("digital employee state service", () => {
|
||||
expect(mockState.db?.events.get("run-governance:provide_task_input:command-provide-input")).toMatchObject({
|
||||
kind: "governance_provide_task_input",
|
||||
});
|
||||
expect(mockState.db?.outbox.get("event:insert:run-governance:provide_task_input:command-provide-input")).toMatchObject({
|
||||
entity_type: "event",
|
||||
entity_id: "run-governance:provide_task_input:command-provide-input",
|
||||
});
|
||||
expect(mockState.db?.events.get("run-governance:resume_task:command-resume-task")).toMatchObject({
|
||||
kind: "governance_resume_task",
|
||||
});
|
||||
|
||||
@@ -533,6 +533,27 @@ describe("digital employee sync service", () => {
|
||||
status: "failed",
|
||||
reason: "computer chat failed",
|
||||
});
|
||||
insertEventEntity("event-governance-input", "governance_provide_task_input", {
|
||||
commandKind: "provide_task_input",
|
||||
commandId: "command-provide-input",
|
||||
planId: "plan-1",
|
||||
taskId: "task-1",
|
||||
runId: "run-1",
|
||||
accepted: true,
|
||||
payload: {
|
||||
input: "请改用客户备用手机号",
|
||||
source: "notification_reply",
|
||||
notification_id: "input:task-1",
|
||||
plan_id: "plan-1",
|
||||
task_id: "task-1",
|
||||
},
|
||||
result: {
|
||||
plan_id: "plan-1",
|
||||
task_id: "task-1",
|
||||
run_id: "run-1",
|
||||
source: "qimingclaw-task-agent",
|
||||
},
|
||||
});
|
||||
insertEventEntity("event-notification-read", "digital_workday_notification_read", {
|
||||
metadata: {
|
||||
notification_id: "task:task-1",
|
||||
@@ -604,6 +625,7 @@ describe("digital employee sync service", () => {
|
||||
{ entity_type: "event", entity_id: "event-artifact" },
|
||||
{ entity_type: "event", entity_id: "event-skill" },
|
||||
{ entity_type: "event", entity_id: "event-dispatch" },
|
||||
{ entity_type: "event", entity_id: "event-governance-input" },
|
||||
{ entity_type: "event", entity_id: "event-notification-read" },
|
||||
{ entity_type: "event", entity_id: "event-notification-dismiss" },
|
||||
{ entity_type: "event", entity_id: "event-notification-reply" },
|
||||
@@ -668,6 +690,16 @@ describe("digital employee sync service", () => {
|
||||
status: "failed",
|
||||
reason: "computer chat failed",
|
||||
});
|
||||
expect(businessView("event-governance-input")).toMatchObject({
|
||||
category: "governance",
|
||||
command_kind: "provide_task_input",
|
||||
plan_id: "plan-1",
|
||||
task_id: "task-1",
|
||||
source: "notification_reply",
|
||||
notification_id: "input:task-1",
|
||||
input_length: 10,
|
||||
});
|
||||
expect(businessView("event-governance-input")).not.toHaveProperty("input");
|
||||
expect(businessView("event-notification-read")).toMatchObject({
|
||||
category: "notification",
|
||||
notification_id: "task:task-1",
|
||||
|
||||
@@ -568,6 +568,7 @@ function eventSpecificBusinessView(
|
||||
}
|
||||
if (kind.startsWith("skill_call_")) return skillCallBusinessView(payload);
|
||||
if (kind.startsWith("plan_step_dispatch_")) return planStepDispatchBusinessView(payload);
|
||||
if (kind === "governance_provide_task_input") return governanceProvideTaskInputBusinessView(payload);
|
||||
if (isNotificationEvent(kind)) return notificationBusinessView(payload);
|
||||
return {};
|
||||
}
|
||||
@@ -638,6 +639,20 @@ function planStepDispatchBusinessView(payload: Record<string, unknown>): Record<
|
||||
};
|
||||
}
|
||||
|
||||
function governanceProvideTaskInputBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
|
||||
const commandPayload = objectRecord(payload.payload) ?? {};
|
||||
const result = objectRecord(payload.result) ?? {};
|
||||
const input = stringField(commandPayload.input ?? payload.input);
|
||||
return {
|
||||
command_kind: stringField(payload.commandKind ?? payload.command_kind) || "provide_task_input",
|
||||
plan_id: stringField(payload.planId ?? payload.plan_id ?? commandPayload.plan_id ?? result.plan_id) || null,
|
||||
task_id: stringField(payload.taskId ?? payload.task_id ?? commandPayload.task_id ?? result.task_id) || null,
|
||||
source: stringField(commandPayload.source ?? result.source ?? payload.source) || null,
|
||||
notification_id: stringField(commandPayload.notification_id ?? payload.notification_id) || null,
|
||||
input_length: input ? input.length : null,
|
||||
};
|
||||
}
|
||||
|
||||
function notificationBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
|
||||
const metadata = objectRecord(payload.metadata) ?? {};
|
||||
const state = objectRecord(payload.state) ?? {};
|
||||
|
||||
@@ -583,9 +583,9 @@ GET /api/digital-employee/approvals
|
||||
- 建议:下一轮先补管理端审批历史与通知订阅策略,再做多步审批和风险策略联动。
|
||||
|
||||
9. **通知 / 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 并提炼状态字段,通知投影、本地订阅策略与管理端消费模型已开始闭环。
|
||||
- 缺口:仍缺少系统级桌面通知、真实管理端通知页面、跨端未读拉取回写和任务输入自动转化。
|
||||
- 建议:下一轮围绕跨端未读同步和任务输入自动转化,把本地通知业务视图推进到真正跨端协同入口。
|
||||
- 已吸收:运行事件和同步失败能在数字员工页面展示;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 并提炼状态字段;`need_input` 通知首次回复会同步转化为 `provide_task_input` 治理命令,保留通知回复 overlay 的同时写入 Task/Run/Event/outbox,管理端 sync `business_view` 只同步来源、通知 ID 和输入长度,通知投影、本地订阅策略与任务输入链路已开始闭环。
|
||||
- 缺口:仍缺少系统级桌面通知、真实管理端通知页面和跨端未读拉取回写。
|
||||
- 建议:下一轮围绕跨端未读同步和系统桌面通知,把本地通知业务视图推进到真正跨端协同入口。
|
||||
|
||||
10. **入口路由 / 任务分流(路由业务视图与通知联动已开始)**
|
||||
- 已吸收:管理端通过 `/computer/chat` 进入 qimingclaw 后会生成本地任务事实;embedded `/api/ingress/route`、`/api/route-decisions` 和 `/api/digital-employee/route-decisions` 已由 qimingclaw adapter 接管。
|
||||
|
||||
Reference in New Issue
Block a user