@xopcai/xopc 0.0.227 → 0.0.228
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/gateway/static/root/assets/{activity-store-BevwE16n.js → activity-store-M0nAY5XB.js} +1 -1
- package/dist/gateway/static/root/assets/{agent-avatar-dicebear-BggVQrEZ.js → agent-avatar-dicebear-CjGZBs4C.js} +1 -1
- package/dist/gateway/static/root/assets/agents-B0NI4_NI.js +68 -0
- package/dist/gateway/static/root/assets/{api-Bx_BpIt8.js → api-BywYxpaS.js} +1 -1
- package/dist/gateway/static/root/assets/{app-management-settings-panel-BtKP9n2r.js → app-management-settings-panel--SjycrXk.js} +1 -1
- package/dist/gateway/static/root/assets/{appearance-settings-BoH2NAUX.js → appearance-settings-8G-TVUtg.js} +1 -1
- package/dist/gateway/static/root/assets/apps-page-2NiqD-YP.js +1 -0
- package/dist/gateway/static/root/assets/{archive-plugin-DnceUyxm.js → archive-plugin-7_DXaMTP.js} +1 -1
- package/dist/gateway/static/root/assets/{attachment-load-Bh8_9gkr.js → attachment-load-Bx44zSpz.js} +1 -1
- package/dist/gateway/static/root/assets/{automations-page-ZKeAAVxD.js → automations-page-CrKAHqc0.js} +1 -1
- package/dist/gateway/static/root/assets/automations-workspace-Q3g9cf0h.js +6 -0
- package/dist/gateway/static/root/assets/{binary-plugins-Cy9er6w-.js → binary-plugins-DjDwnHAQ.js} +1 -1
- package/dist/gateway/static/root/assets/{block-editor-Cc0wWvNX.js → block-editor-7aMOKoZM.js} +3 -3
- package/dist/gateway/static/root/assets/browser-settings-page-DTwPkmAC.js +8 -0
- package/dist/gateway/static/root/assets/{browser-workflow-inputs-BS7rpLLl.js → browser-workflow-inputs-8NC2qDRF.js} +1 -1
- package/dist/gateway/static/root/assets/{browser-workflows-page-BdBVUZXm.js → browser-workflows-page-DVLrWKz_.js} +2 -2
- package/dist/gateway/static/root/assets/capabilities-settings-panel-YcP4FbN7.js +7 -0
- package/dist/gateway/static/root/assets/{capability-presets-api-D1Lqz0Tt.js → capability-presets-api-o0y9WgAI.js} +1 -1
- package/dist/gateway/static/root/assets/capability-presets-settings-panel-AcPjHHia.js +3 -0
- package/dist/gateway/static/root/assets/{channel-recipient-api-CGuEl-l2.js → channel-recipient-api-C5ABr73b.js} +1 -1
- package/dist/gateway/static/root/assets/channels-settings-Ca94Q1D-.js +1 -0
- package/dist/gateway/static/root/assets/channels-status-swr-38FsOnR0.js +1 -0
- package/dist/gateway/static/root/assets/{chat-terminal-dock-CdX_7aQh.js → chat-terminal-dock-BR-_7KC0.js} +2 -2
- package/dist/gateway/static/root/assets/connectors-page-SGxfLTd6.js +2 -0
- package/dist/gateway/static/root/assets/{cron-expression-BnSQc4Ek.js → cron-expression-D8-c-Tkw.js} +1 -1
- package/dist/gateway/static/root/assets/{date-picker-BbuG7zQ_.js → date-picker-kV-Jyl9J.js} +1 -1
- package/dist/gateway/static/root/assets/{dependency-picker-snZeqOmG.js → dependency-picker-CwKcrfGj.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-DJQ8JoR1.js → desktop-pet-D9rnchu3.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-settings-B9txsRtv.js → desktop-pet-settings-CTs5WGzk.js} +1 -1
- package/dist/gateway/static/root/assets/{directory-picker-path-field-D7WKWkNB.js → directory-picker-path-field-CCl0aaDm.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-D_7cOCj2.js → extension-debug-page-Cau40SvC.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-Bwu47iUb.js → extension-page-BsJXdvIL.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-Dwg4dSZM.js → extension-settings-page-qWqJyF7-.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-config-api-jURUSte_.js → gateway-config-api-BI0SELXm.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-config-swr-6SQXF1yR.js → gateway-config-swr-BWGjnmDl.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-settings-UklJOMLX.js → gateway-settings-6ZdOIC7F.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-startup-retry-DqWaxyvZ.js → gateway-startup-retry-B_zrtccP.js} +1 -1
- package/dist/gateway/static/root/assets/{heartbeat-settings-Djs_nduD.js → heartbeat-settings-KBwhXeFD.js} +1 -1
- package/dist/gateway/static/root/assets/home-page-BA2M4CLi.js +1 -0
- package/dist/gateway/static/root/assets/{image-generation-api-Bxmt6LEr.js → image-generation-api-YKP2me5Z.js} +1 -1
- package/dist/gateway/static/root/assets/index-BX0dz1JK.js +70 -0
- package/dist/gateway/static/root/assets/index-D_hrEKXM.css +2 -0
- package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-ByVsCH3d.js → keyboard-shortcuts-settings-kHtpnyEm.js} +1 -1
- package/dist/gateway/static/root/assets/local-app-workbench-page-D2nkn09R.js +2 -0
- package/dist/gateway/static/root/assets/{local-apps-page-D19R-xsz.js → local-apps-page-BCXa0oUa.js} +1 -1
- package/dist/gateway/static/root/assets/{locale-store-Ci1Qi0Q-.js → locale-store-CXhjfe4q.js} +1 -1
- package/dist/gateway/static/root/assets/logs-page-D3sNst0K.js +2 -0
- package/dist/gateway/static/root/assets/{management-settings-G0y7zmfY.js → management-settings-tHnlfOAQ.js} +1 -1
- package/dist/gateway/static/root/assets/{markdown-split-B69d2HWt.js → markdown-split-BMLqHHr5.js} +1 -1
- package/dist/gateway/static/root/assets/{markdown-view-DD_PgVue.css → markdown-view-CLzTdVLW.css} +1 -1
- package/dist/gateway/static/root/assets/markdown-view-DvM5PWEH.js +201 -0
- package/dist/gateway/static/root/assets/{media-plugins-6TWEQgN_.js → media-plugins-DybwqTgv.js} +1 -1
- package/dist/gateway/static/root/assets/messages-BMdD0LWk.js +3 -0
- package/dist/gateway/static/root/assets/{notes-api-DLsnnZCS.js → notes-api-BFsOGYkd.js} +1 -1
- package/dist/gateway/static/root/assets/{notes-page-CkRJJKDm.js → notes-page-C24SfF6r.js} +1 -1
- package/dist/gateway/static/root/assets/notes-workbench-CTdZc3ov.js +4 -0
- package/dist/gateway/static/root/assets/{product-open-page-HxArIvoN.js → product-open-page-DBfCVlJq.js} +1 -1
- package/dist/gateway/static/root/assets/project-detail-page-COHaEpbA.js +1 -0
- package/dist/gateway/static/root/assets/projects-page-lMnST7D_.js +1 -0
- package/dist/gateway/static/root/assets/{registry-api-lEar5FOV.js → registry-api-BFZrVYfy.js} +1 -1
- package/dist/gateway/static/root/assets/remote-access-hub-BIRKBvO_.js +1 -0
- package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-Bf03UNGD.js → runtime-tools-settings-panel-BPEGTip5.js} +1 -1
- package/dist/gateway/static/root/assets/{schema-form-CIXMBgx5.js → schema-form-Dxlu60Td.js} +1 -1
- package/dist/gateway/static/root/assets/sessions-page-CATO9G3y.js +1 -0
- package/dist/gateway/static/root/assets/settings-advanced-gate-Cco_W41x.js +1 -0
- package/dist/gateway/static/root/assets/{settings-form-section-DgdB26mE.js → settings-form-section-ChXh3Bwk.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-loading-skeleton-efk5W6lG.js → settings-loading-skeleton-BCAUqIe2.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-page-BHVf4gEP.js → settings-page-D25W9Okr.js} +1 -1
- package/dist/gateway/static/root/assets/{setup-status-panel-WK0dGNMv.js → setup-status-panel-DsN7Z4Fl.js} +1 -1
- package/dist/gateway/static/root/assets/{share-preview-page-Cod-w2qm.js → share-preview-page-CRFnUaNb.js} +1 -1
- package/dist/gateway/static/root/assets/{shares-settings-CUqQ31gt.js → shares-settings-CAX0eNaP.js} +1 -1
- package/dist/gateway/static/root/assets/skill-reload-api-Doy1gghe.js +1 -0
- package/dist/gateway/static/root/assets/{skills-page-DwCcipqK.js → skills-page-Cl7jjTK7.js} +1 -1
- package/dist/gateway/static/root/assets/{skills-page.utils-3WSbtl3S.js → skills-page.utils-BiCl4nb5.js} +1 -1
- package/dist/gateway/static/root/assets/{system-settings-panel-C5VxqC_s.js → system-settings-panel-BuESR7ZT.js} +1 -1
- package/dist/gateway/static/root/assets/task-detail-page-BRiy8NL3.js +1 -0
- package/dist/gateway/static/root/assets/{theme-store-Dd89g_ki.js → theme-store-DOdCjyuw.js} +1 -1
- package/dist/gateway/static/root/assets/{use-autosave-CeV8VJh2.js → use-autosave-Dg1mgJ_l.js} +1 -1
- package/dist/gateway/static/root/assets/user-context-page-CqoADNKu.js +1 -0
- package/dist/gateway/static/root/assets/{voice-api-key-field-EmJ-8aZu.js → voice-api-key-field-BDNexlVR.js} +1 -1
- package/dist/gateway/static/root/assets/{work-discovery-overlay-zsk6E3Mi.js → work-discovery-overlay-BHTfeaWL.js} +1 -1
- package/dist/gateway/static/root/assets/workflow-run-setup-panel-Cf_w-NZy.js +3 -0
- package/dist/gateway/static/root/assets/workflows-page-j0P-uECz.js +5 -0
- package/dist/gateway/static/root/index.html +12 -12
- package/dist/gateway/static/root/notification-sw.js +1 -1
- package/dist/package.js +1 -1
- package/dist/packages/gateway-contract/src/notifications.js +56 -0
- package/dist/packages/gateway-contract/src/turn-outcome.js +114 -0
- package/dist/src/agent/lifecycle/timeout-wrapper.d.ts +1 -0
- package/dist/src/agent/lifecycle/timeout-wrapper.js +7 -6
- package/dist/src/agent/mcp/mcp-transport-config.d.ts +1 -1
- package/dist/src/agent/mcp/mcp-transport-config.js +1 -1
- package/dist/src/agent/orchestration/run-agent-turn-with-timeout.d.ts +2 -2
- package/dist/src/agent/orchestration/run-agent-turn-with-timeout.js +4 -4
- package/dist/src/agent/tools/create-share-tool.js +14 -1
- package/dist/src/agent/tools/exec-command.js +3 -3
- package/dist/src/agent/tools/execute-code-tool.js +1 -1
- package/dist/src/agent/tools/executor.js +2 -2
- package/dist/src/agent/tools/image-generate-tool.js +12 -0
- package/dist/src/agent/tools/send-media.js +11 -0
- package/dist/src/agent/tools/xopc-use-tool.js +1 -1
- package/dist/src/cli/command-loaders.js +1 -0
- package/dist/src/cli/command-manifest.js +4 -0
- package/dist/src/cli/commands/doctor/checks/database-schema.js +1 -1
- package/dist/src/cli/commands/history.d.ts +1 -0
- package/dist/src/cli/commands/history.js +50 -0
- package/dist/src/config/agent-profile.js +1 -1
- package/dist/src/gateway/agents-admin.js +1 -1
- package/dist/src/gateway/chat-stream/protocol.d.ts +3 -2
- package/dist/src/gateway/chat-stream/turn-outcome-collector.d.ts +16 -0
- package/dist/src/gateway/chat-stream/turn-outcome-collector.js +111 -0
- package/dist/src/gateway/hono/lib/static-ui.js +7 -5
- package/dist/src/gateway/hono/routes/index.js +2 -0
- package/dist/src/gateway/hono/routes/mobile.js +18 -40
- package/dist/src/gateway/hono/routes/notifications.d.ts +3 -0
- package/dist/src/gateway/hono/routes/notifications.js +45 -0
- package/dist/src/gateway/hono/routes/sessions.js +31 -0
- package/dist/src/gateway/hono/routes/tasks.js +1 -1
- package/dist/src/gateway/service/agent-runner.js +6 -2
- package/dist/src/gateway/service/run-gateway-agent.js +53 -1
- package/dist/src/gateway/service/sessions-api.d.ts +6 -0
- package/dist/src/gateway/service/sessions-api.js +60 -3
- package/dist/src/gateway/service.d.ts +2 -2
- package/dist/src/gateway/service.js +8 -7
- package/dist/src/history/ctx/exporter.d.ts +14 -0
- package/dist/src/history/ctx/exporter.js +99 -0
- package/dist/src/history/ctx/format.d.ts +25 -0
- package/dist/src/history/ctx/format.js +282 -0
- package/dist/src/media/session-references.d.ts +6 -0
- package/dist/src/media/session-references.js +24 -3
- package/dist/src/notifications/device-store.d.ts +16 -0
- package/dist/src/notifications/device-store.js +96 -0
- package/dist/src/notifications/planner.d.ts +6 -0
- package/dist/src/notifications/planner.js +156 -0
- package/dist/src/notifications/service.d.ts +18 -0
- package/dist/src/notifications/service.js +184 -0
- package/dist/src/notifications/store.d.ts +53 -0
- package/dist/src/notifications/store.js +184 -0
- package/dist/src/notifications/types.d.ts +29 -0
- package/dist/src/notifications/types.js +14 -0
- package/dist/src/session/client-history.d.ts +3 -1
- package/dist/src/session/client-history.js +13 -0
- package/dist/src/session/index.d.ts +1 -1
- package/dist/src/session/manager.d.ts +1 -0
- package/dist/src/session/manager.js +6 -0
- package/dist/src/session/store.d.ts +15 -0
- package/dist/src/session/store.js +58 -0
- package/dist/src/storage/sqlite/index.d.ts +1 -1
- package/dist/src/storage/sqlite/index.js +2 -2
- package/dist/src/storage/sqlite/migrations/136_product_notifications.sql +79 -0
- package/dist/src/storage/sqlite/migrations/runner.d.ts +1 -1
- package/dist/src/storage/sqlite/migrations/runner.js +3 -3
- package/dist/src/storage/sqlite/migrations/runner.ts +1 -1
- package/dist/src/storage/sqlite/schema.js +1 -1
- package/dist/src/storage/sqlite/session-input-repository.d.ts +5 -0
- package/dist/src/storage/sqlite/session-input-repository.js +9 -1
- package/dist/src/tasks/index.js +2 -2
- package/dist/src/tasks/task-application-service.js +42 -6
- package/dist/src/tasks/task-change-events.d.ts +9 -0
- package/dist/src/tasks/task-change-events.js +17 -1
- package/dist/src/tasks/task-outbox-dispatcher.js +5 -1
- package/dist/src/tasks/task-run-dispatcher.js +4 -2
- package/dist/src/tasks/task-run-repository.js +13 -0
- package/package.json +2 -2
- package/dist/gateway/static/root/assets/agents-TJpjRHnr.js +0 -68
- package/dist/gateway/static/root/assets/apps-page-CZ4Da51l.js +0 -1
- package/dist/gateway/static/root/assets/automations-workspace-2Y00PvJU.js +0 -6
- package/dist/gateway/static/root/assets/browser-settings-page-CbJD27_j.js +0 -8
- package/dist/gateway/static/root/assets/capabilities-settings-panel-Dv8y6OuP.js +0 -7
- package/dist/gateway/static/root/assets/capability-presets-settings-panel-TZmjzWzt.js +0 -3
- package/dist/gateway/static/root/assets/channels-settings-g-68dutA.js +0 -1
- package/dist/gateway/static/root/assets/channels-status-swr-D7KsAeWh.js +0 -1
- package/dist/gateway/static/root/assets/connectors-page-DXA0rAvQ.js +0 -2
- package/dist/gateway/static/root/assets/home-page-i6bXCHdo.js +0 -1
- package/dist/gateway/static/root/assets/index-BBBE89vm.css +0 -2
- package/dist/gateway/static/root/assets/index-BsD5wygl.js +0 -74
- package/dist/gateway/static/root/assets/local-app-workbench-page-BCKEDMlP.js +0 -2
- package/dist/gateway/static/root/assets/logs-page-Cy9YQK7j.js +0 -2
- package/dist/gateway/static/root/assets/markdown-view-CBM-v2ph.js +0 -69
- package/dist/gateway/static/root/assets/mermaid-render-BbqDc_LQ.js +0 -134
- package/dist/gateway/static/root/assets/messages-CtOJwGW3.js +0 -3
- package/dist/gateway/static/root/assets/notes-workbench-B10T3hGX.js +0 -4
- package/dist/gateway/static/root/assets/project-detail-page-DsqRlgsy.js +0 -1
- package/dist/gateway/static/root/assets/projects-page-Bu2PN8OK.js +0 -1
- package/dist/gateway/static/root/assets/remote-access-hub-CXACdJ2f.js +0 -1
- package/dist/gateway/static/root/assets/sessions-page-CS2knWDT.js +0 -1
- package/dist/gateway/static/root/assets/settings-advanced-gate-ByFGk0q_.js +0 -1
- package/dist/gateway/static/root/assets/skill-reload-api-DweAPjI8.js +0 -1
- package/dist/gateway/static/root/assets/task-detail-page-DEj-BFMP.js +0 -1
- package/dist/gateway/static/root/assets/user-context-page-f7yh7aD5.js +0 -1
- package/dist/gateway/static/root/assets/workflow-run-setup-panel-CBAYvm3G.js +0 -3
- package/dist/gateway/static/root/assets/workflows-page-BN0JmHrG.js +0 -5
- package/dist/src/mobile/notification-service.d.ts +0 -9
- package/dist/src/mobile/notification-service.js +0 -156
- package/dist/src/mobile/notification-store.d.ts +0 -28
- package/dist/src/mobile/notification-store.js +0 -132
- package/dist/src/mobile/notification-types.d.ts +0 -37
- package/dist/src/mobile/notification-types.js +0 -9
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { NotificationTargetSchema } from "../../packages/gateway-contract/src/notifications.js";
|
|
2
|
+
//#region src/notifications/planner.ts
|
|
3
|
+
function chatPlan(payload) {
|
|
4
|
+
if (!payload || typeof payload !== "object") return null;
|
|
5
|
+
const event = payload;
|
|
6
|
+
if (event.status !== "success" && event.status !== "error") return null;
|
|
7
|
+
if (typeof event.runId !== "string" || !event.runId) return null;
|
|
8
|
+
const target = NotificationTargetSchema.safeParse(event.target);
|
|
9
|
+
if (!target.success || target.data.kind !== "chat" || target.data.sessionKey !== event.sessionKey) return null;
|
|
10
|
+
const completed = event.status === "success";
|
|
11
|
+
const type = completed ? "chat.completed" : "chat.failed";
|
|
12
|
+
const fallback = {
|
|
13
|
+
en: "Open the conversation to review the result.",
|
|
14
|
+
zh: "打开对话查看结果。"
|
|
15
|
+
};
|
|
16
|
+
const sessionTitle = typeof event.sessionTitle === "string" ? event.sessionTitle.trim().slice(0, 120) : "";
|
|
17
|
+
return {
|
|
18
|
+
dedupeKey: `${type}:${event.runId}`,
|
|
19
|
+
notification: {
|
|
20
|
+
type,
|
|
21
|
+
target: target.data,
|
|
22
|
+
priority: completed ? "normal" : "high",
|
|
23
|
+
title: completed ? {
|
|
24
|
+
en: "Response ready",
|
|
25
|
+
zh: "回答已就绪"
|
|
26
|
+
} : {
|
|
27
|
+
en: "Response needs attention",
|
|
28
|
+
zh: "回答需要处理"
|
|
29
|
+
},
|
|
30
|
+
body: sessionTitle ? {
|
|
31
|
+
en: sessionTitle,
|
|
32
|
+
zh: sessionTitle
|
|
33
|
+
} : fallback,
|
|
34
|
+
payload: { runId: event.runId }
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function taskPlan(payload) {
|
|
39
|
+
if (!payload || typeof payload !== "object") return null;
|
|
40
|
+
const data = payload;
|
|
41
|
+
const taskId = typeof data.task?.id === "string" ? data.task.id : typeof data.taskId === "string" ? data.taskId : "";
|
|
42
|
+
const sourceEventId = typeof data.sourceEventId === "string" ? data.sourceEventId : "";
|
|
43
|
+
if (!taskId || !sourceEventId) return null;
|
|
44
|
+
const type = data.reason === "blocked" ? "task.blocked" : data.reason === "failed" ? "task.failed" : data.reason === "user_input" || data.reason === "approval" ? "task.needs_input" : data.to === "closed" && data.resolution === "done" ? "task.completed" : void 0;
|
|
45
|
+
if (!type) return null;
|
|
46
|
+
const taskTitle = typeof data.task?.title === "string" ? data.task.title.trim().slice(0, 120) : "";
|
|
47
|
+
const title = type === "task.needs_input" ? {
|
|
48
|
+
en: "Action needed",
|
|
49
|
+
zh: "需要你的操作"
|
|
50
|
+
} : type === "task.blocked" ? {
|
|
51
|
+
en: "Task blocked",
|
|
52
|
+
zh: "任务受阻"
|
|
53
|
+
} : type === "task.failed" ? {
|
|
54
|
+
en: "Task failed",
|
|
55
|
+
zh: "任务失败"
|
|
56
|
+
} : {
|
|
57
|
+
en: "Task completed",
|
|
58
|
+
zh: "任务已完成"
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
dedupeKey: `${type}:${sourceEventId}`,
|
|
62
|
+
notification: {
|
|
63
|
+
type,
|
|
64
|
+
target: {
|
|
65
|
+
kind: "task",
|
|
66
|
+
taskId
|
|
67
|
+
},
|
|
68
|
+
priority: type === "task.completed" ? "normal" : "high",
|
|
69
|
+
title,
|
|
70
|
+
...taskTitle ? { body: {
|
|
71
|
+
en: taskTitle,
|
|
72
|
+
zh: taskTitle
|
|
73
|
+
} } : {},
|
|
74
|
+
payload: {
|
|
75
|
+
taskId,
|
|
76
|
+
sourceEventId
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function automationPlan(payload) {
|
|
82
|
+
if (!payload || typeof payload !== "object") return null;
|
|
83
|
+
const event = payload;
|
|
84
|
+
if (event.notificationPolicy === "none") return null;
|
|
85
|
+
if (event.notificationPolicy === "attention" && event.requiresAttention !== true) return null;
|
|
86
|
+
const run = event.run;
|
|
87
|
+
if (!run || run.status !== "failed" && run.status !== "timeout" && run.status !== "succeeded") return null;
|
|
88
|
+
const completed = run.status === "succeeded";
|
|
89
|
+
const type = completed ? "automation.completed" : "automation.failed";
|
|
90
|
+
return {
|
|
91
|
+
dedupeKey: `${type}:${run.id}`,
|
|
92
|
+
notification: {
|
|
93
|
+
type,
|
|
94
|
+
target: {
|
|
95
|
+
kind: "automation_run",
|
|
96
|
+
automationId: run.automationId,
|
|
97
|
+
runId: run.id
|
|
98
|
+
},
|
|
99
|
+
priority: completed ? "normal" : "high",
|
|
100
|
+
title: completed ? {
|
|
101
|
+
en: "Automation completed",
|
|
102
|
+
zh: "自动化已完成"
|
|
103
|
+
} : {
|
|
104
|
+
en: "Automation needs attention",
|
|
105
|
+
zh: "自动化需要处理"
|
|
106
|
+
},
|
|
107
|
+
body: {
|
|
108
|
+
en: (run.error || run.summary || run.automationName).slice(0, 180),
|
|
109
|
+
zh: (run.error || run.summary || run.automationName).slice(0, 180)
|
|
110
|
+
},
|
|
111
|
+
payload: {
|
|
112
|
+
automationId: run.automationId,
|
|
113
|
+
runId: run.id
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function proactivePlan(payload) {
|
|
119
|
+
if (!payload || typeof payload !== "object") return null;
|
|
120
|
+
const item = payload;
|
|
121
|
+
if (typeof item.id !== "string" || typeof item.insight?.id !== "string" || typeof item.insight.title !== "string") return null;
|
|
122
|
+
const title = item.insight.title.trim().slice(0, 120);
|
|
123
|
+
const summary = typeof item.insight.summary === "string" ? item.insight.summary.trim().slice(0, 180) : "";
|
|
124
|
+
return {
|
|
125
|
+
dedupeKey: `proactive.insight:${item.id}`,
|
|
126
|
+
notification: {
|
|
127
|
+
type: "proactive.insight",
|
|
128
|
+
target: {
|
|
129
|
+
kind: "insight",
|
|
130
|
+
inboxItemId: item.id
|
|
131
|
+
},
|
|
132
|
+
priority: item.insight.urgency === "high" || item.insight.urgency === "critical" ? "high" : "normal",
|
|
133
|
+
title: {
|
|
134
|
+
en: title,
|
|
135
|
+
zh: title
|
|
136
|
+
},
|
|
137
|
+
...summary ? { body: {
|
|
138
|
+
en: summary,
|
|
139
|
+
zh: summary
|
|
140
|
+
} } : {},
|
|
141
|
+
payload: {
|
|
142
|
+
inboxItemId: item.id,
|
|
143
|
+
insightId: item.insight.id
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function notificationPlanFromGatewayEvent(type, payload) {
|
|
149
|
+
if (type === "agent.run.ended") return chatPlan(payload);
|
|
150
|
+
if (type === "task.attention_required.v2" || type === "task.phase_changed.v2") return taskPlan(payload);
|
|
151
|
+
if (type === "automation.run.completed") return automationPlan(payload);
|
|
152
|
+
if (type === "proactive.inbox.created") return proactivePlan(payload);
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
156
|
+
export { notificationPlanFromGatewayEvent };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare class NotificationService {
|
|
2
|
+
private readonly options;
|
|
3
|
+
private timer;
|
|
4
|
+
private draining;
|
|
5
|
+
private lastMaintenanceAt;
|
|
6
|
+
constructor(options: {
|
|
7
|
+
publish: (type: string, payload: unknown) => void;
|
|
8
|
+
fetch?: typeof fetch;
|
|
9
|
+
});
|
|
10
|
+
start(): void;
|
|
11
|
+
stop(): void;
|
|
12
|
+
handleGatewayEvent(type: string, payload: unknown): void;
|
|
13
|
+
drain(): Promise<void>;
|
|
14
|
+
private deliverPending;
|
|
15
|
+
private send;
|
|
16
|
+
private checkReceipts;
|
|
17
|
+
private retryOrFail;
|
|
18
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { createLogger } from "../utils/logger/index.js";
|
|
2
|
+
import { init_logger } from "../utils/logger.js";
|
|
3
|
+
import { localizeNotification } from "../../packages/gateway-contract/src/notifications.js";
|
|
4
|
+
import { disableNotificationDeviceForPushToken, listDeliverableNotificationDevices } from "./device-store.js";
|
|
5
|
+
import { notificationPlanFromGatewayEvent } from "./planner.js";
|
|
6
|
+
import { createNotificationEvent, expireUndeliverableNotificationDeliveries, listDueNotificationDeliveries, markNotificationDeliveryAccepted, markNotificationDeliveryDead, markNotificationDeliveryDelivered, pruneNotificationEvents, rescheduleNotificationDelivery } from "./store.js";
|
|
7
|
+
//#region src/notifications/service.ts
|
|
8
|
+
init_logger();
|
|
9
|
+
const log = createLogger("Notifications");
|
|
10
|
+
const EXPO_PUSH_URL = "https://exp.host/--/api/v2/push/send";
|
|
11
|
+
const EXPO_RECEIPTS_URL = "https://exp.host/--/api/v2/push/getReceipts";
|
|
12
|
+
const MAX_DELIVERY_ATTEMPTS = 5;
|
|
13
|
+
const RETRY_DELAYS_MS = [
|
|
14
|
+
3e4,
|
|
15
|
+
12e4,
|
|
16
|
+
6e5,
|
|
17
|
+
36e5,
|
|
18
|
+
216e5
|
|
19
|
+
];
|
|
20
|
+
const RECEIPT_DELAY_MS = 15 * 6e4;
|
|
21
|
+
function preferenceAllows(type, preferences) {
|
|
22
|
+
switch (type) {
|
|
23
|
+
case "chat.completed": return preferences.chatCompleted;
|
|
24
|
+
case "chat.failed": return preferences.chatFailed;
|
|
25
|
+
case "task.needs_input": return preferences.taskNeedsInput;
|
|
26
|
+
case "task.blocked": return preferences.taskBlocked;
|
|
27
|
+
case "task.failed": return preferences.taskFailed;
|
|
28
|
+
case "task.completed": return preferences.taskCompleted;
|
|
29
|
+
case "automation.completed": return preferences.automationCompleted;
|
|
30
|
+
case "automation.failed": return preferences.automationFailed;
|
|
31
|
+
case "proactive.insight": return preferences.proactiveInsight;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function retryAt(attempts, now) {
|
|
35
|
+
return now + RETRY_DELAYS_MS[Math.min(attempts, RETRY_DELAYS_MS.length - 1)];
|
|
36
|
+
}
|
|
37
|
+
function expoError(result) {
|
|
38
|
+
return result.details?.error || result.message || "Expo rejected the notification";
|
|
39
|
+
}
|
|
40
|
+
var NotificationService = class {
|
|
41
|
+
timer = null;
|
|
42
|
+
draining = false;
|
|
43
|
+
lastMaintenanceAt = 0;
|
|
44
|
+
constructor(options) {
|
|
45
|
+
this.options = options;
|
|
46
|
+
}
|
|
47
|
+
start() {
|
|
48
|
+
if (this.timer) return;
|
|
49
|
+
this.drain();
|
|
50
|
+
this.timer = setInterval(() => void this.drain(), 3e4);
|
|
51
|
+
this.timer.unref?.();
|
|
52
|
+
}
|
|
53
|
+
stop() {
|
|
54
|
+
if (this.timer) clearInterval(this.timer);
|
|
55
|
+
this.timer = null;
|
|
56
|
+
}
|
|
57
|
+
handleGatewayEvent(type, payload) {
|
|
58
|
+
try {
|
|
59
|
+
const plan = notificationPlanFromGatewayEvent(type, payload);
|
|
60
|
+
if (!plan) return;
|
|
61
|
+
const devices = listDeliverableNotificationDevices().filter((device) => preferenceAllows(plan.notification.type, device.preferences));
|
|
62
|
+
const result = createNotificationEvent({
|
|
63
|
+
...plan,
|
|
64
|
+
deviceIds: devices.map((device) => device.id)
|
|
65
|
+
});
|
|
66
|
+
if (!result.created) return;
|
|
67
|
+
this.options.publish("notification.created", result.notification);
|
|
68
|
+
this.drain();
|
|
69
|
+
} catch (err) {
|
|
70
|
+
log.error({
|
|
71
|
+
err,
|
|
72
|
+
eventType: type
|
|
73
|
+
}, "Notification event persistence failed");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async drain() {
|
|
77
|
+
if (this.draining) return;
|
|
78
|
+
this.draining = true;
|
|
79
|
+
try {
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
if (now - this.lastMaintenanceAt >= 3600 * 1e3) {
|
|
82
|
+
expireUndeliverableNotificationDeliveries(now);
|
|
83
|
+
pruneNotificationEvents(now - 720 * 60 * 60 * 1e3);
|
|
84
|
+
this.lastMaintenanceAt = now;
|
|
85
|
+
}
|
|
86
|
+
await this.deliverPending();
|
|
87
|
+
await this.checkReceipts();
|
|
88
|
+
} catch (err) {
|
|
89
|
+
log.warn({ err }, "Notification delivery pass failed");
|
|
90
|
+
} finally {
|
|
91
|
+
this.draining = false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async deliverPending() {
|
|
95
|
+
const deliveries = listDueNotificationDeliveries("pending");
|
|
96
|
+
await Promise.all(deliveries.map((delivery) => this.send(delivery)));
|
|
97
|
+
}
|
|
98
|
+
async send(delivery) {
|
|
99
|
+
const fetchImpl = this.options.fetch ?? fetch;
|
|
100
|
+
const localized = localizeNotification(delivery.event, delivery.locale);
|
|
101
|
+
try {
|
|
102
|
+
const response = await fetchImpl(EXPO_PUSH_URL, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: {
|
|
105
|
+
"content-type": "application/json",
|
|
106
|
+
accept: "application/json"
|
|
107
|
+
},
|
|
108
|
+
signal: AbortSignal.timeout(1e4),
|
|
109
|
+
body: JSON.stringify({
|
|
110
|
+
to: delivery.pushToken,
|
|
111
|
+
title: localized.localizedTitle,
|
|
112
|
+
body: localized.localizedBody,
|
|
113
|
+
sound: delivery.event.priority === "high" ? "default" : void 0,
|
|
114
|
+
priority: delivery.event.priority,
|
|
115
|
+
data: {
|
|
116
|
+
eventId: delivery.event.id,
|
|
117
|
+
target: delivery.event.target,
|
|
118
|
+
...delivery.event.payload
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
});
|
|
122
|
+
if (!response.ok) throw new Error(`Expo push request failed (${response.status})`);
|
|
123
|
+
const body = await response.json();
|
|
124
|
+
const result = Array.isArray(body.data) ? body.data[0] : body.data;
|
|
125
|
+
if (result?.status !== "ok" || !result.id) {
|
|
126
|
+
const error = expoError(result ?? {});
|
|
127
|
+
if (result?.details?.error === "DeviceNotRegistered") {
|
|
128
|
+
disableNotificationDeviceForPushToken(delivery.pushToken);
|
|
129
|
+
markNotificationDeliveryDead(delivery.event.id, delivery.deviceId, error);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
throw new Error(error);
|
|
133
|
+
}
|
|
134
|
+
markNotificationDeliveryAccepted(delivery.event.id, delivery.deviceId, result.id, Date.now() + RECEIPT_DELAY_MS);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
this.retryOrFail(delivery, err);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async checkReceipts() {
|
|
140
|
+
const withTickets = listDueNotificationDeliveries("accepted").filter((delivery) => delivery.providerTicketId);
|
|
141
|
+
if (withTickets.length === 0) return;
|
|
142
|
+
const fetchImpl = this.options.fetch ?? fetch;
|
|
143
|
+
try {
|
|
144
|
+
const response = await fetchImpl(EXPO_RECEIPTS_URL, {
|
|
145
|
+
method: "POST",
|
|
146
|
+
headers: {
|
|
147
|
+
"content-type": "application/json",
|
|
148
|
+
accept: "application/json"
|
|
149
|
+
},
|
|
150
|
+
signal: AbortSignal.timeout(1e4),
|
|
151
|
+
body: JSON.stringify({ ids: withTickets.map((delivery) => delivery.providerTicketId) })
|
|
152
|
+
});
|
|
153
|
+
if (!response.ok) throw new Error(`Expo receipt request failed (${response.status})`);
|
|
154
|
+
const body = await response.json();
|
|
155
|
+
for (const delivery of withTickets) {
|
|
156
|
+
const result = body.data?.[delivery.providerTicketId];
|
|
157
|
+
if (!result) this.retryOrFail(delivery, /* @__PURE__ */ new Error("Expo receipt is not ready"), "accepted");
|
|
158
|
+
else if (result.status === "ok") markNotificationDeliveryDelivered(delivery.event.id, delivery.deviceId);
|
|
159
|
+
else {
|
|
160
|
+
const error = expoError(result);
|
|
161
|
+
if (result.details?.error === "DeviceNotRegistered") disableNotificationDeviceForPushToken(delivery.pushToken);
|
|
162
|
+
markNotificationDeliveryDead(delivery.event.id, delivery.deviceId, error);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch (err) {
|
|
166
|
+
for (const delivery of withTickets) this.retryOrFail(delivery, err, "accepted");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
retryOrFail(delivery, error, status = "pending") {
|
|
170
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
171
|
+
if (delivery.attempts + 1 >= MAX_DELIVERY_ATTEMPTS) {
|
|
172
|
+
markNotificationDeliveryDead(delivery.event.id, delivery.deviceId, message);
|
|
173
|
+
log.warn({
|
|
174
|
+
eventId: delivery.event.id,
|
|
175
|
+
deviceId: delivery.deviceId,
|
|
176
|
+
errorMessage: message
|
|
177
|
+
}, `Notification delivery exhausted retries: ${message}`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
rescheduleNotificationDelivery(delivery.event.id, delivery.deviceId, status, retryAt(delivery.attempts, Date.now()), message);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
//#endregion
|
|
184
|
+
export { NotificationService };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ProductNotification } from '@xopcai/gateway-contract';
|
|
2
|
+
export type NotificationDelivery = {
|
|
3
|
+
event: ProductNotification;
|
|
4
|
+
deviceId: string;
|
|
5
|
+
pushToken: string;
|
|
6
|
+
locale: 'en' | 'zh';
|
|
7
|
+
attempts: number;
|
|
8
|
+
providerTicketId?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function createNotificationEvent(input: {
|
|
11
|
+
dedupeKey: string;
|
|
12
|
+
notification: Omit<ProductNotification, 'id' | 'createdAt' | 'schemaVersion'>;
|
|
13
|
+
deviceIds: string[];
|
|
14
|
+
createdAt?: number;
|
|
15
|
+
}): {
|
|
16
|
+
notification: ProductNotification;
|
|
17
|
+
created: boolean;
|
|
18
|
+
};
|
|
19
|
+
export declare function listNotificationEvents(options?: {
|
|
20
|
+
afterId?: string;
|
|
21
|
+
since?: number;
|
|
22
|
+
limit?: number;
|
|
23
|
+
}): {
|
|
24
|
+
items: ProductNotification[];
|
|
25
|
+
nextCursor: string | null;
|
|
26
|
+
};
|
|
27
|
+
export declare function acknowledgeNotification(eventId: string, consumerId: string, surface: 'web' | 'electron' | 'mobile'): boolean;
|
|
28
|
+
export declare function listDueNotificationDeliveries(status: 'pending' | 'accepted', now?: number, limit?: number): NotificationDelivery[];
|
|
29
|
+
export declare function markNotificationDeliveryAccepted(eventId: string, deviceId: string, ticketId: string, receiptAt: number): void;
|
|
30
|
+
export declare function markNotificationDeliveryDelivered(eventId: string, deviceId: string): void;
|
|
31
|
+
export declare function markNotificationDeliveryDead(eventId: string, deviceId: string, error: string): void;
|
|
32
|
+
export declare function rescheduleNotificationDelivery(eventId: string, deviceId: string, status: 'pending' | 'accepted', nextAttemptAt: number, error: string): void;
|
|
33
|
+
export declare function notificationDeliveryMetrics(): {
|
|
34
|
+
pending: number;
|
|
35
|
+
accepted: number;
|
|
36
|
+
delivered: number;
|
|
37
|
+
dead: number;
|
|
38
|
+
due: number;
|
|
39
|
+
devices: {
|
|
40
|
+
deliverable: number;
|
|
41
|
+
expired: number;
|
|
42
|
+
disabled: number;
|
|
43
|
+
};
|
|
44
|
+
oldestPendingAt: number | null;
|
|
45
|
+
latestError: {
|
|
46
|
+
eventId: string;
|
|
47
|
+
deviceId: string;
|
|
48
|
+
message: string;
|
|
49
|
+
updatedAt: number;
|
|
50
|
+
} | null;
|
|
51
|
+
};
|
|
52
|
+
export declare function expireUndeliverableNotificationDeliveries(now?: number): number;
|
|
53
|
+
export declare function pruneNotificationEvents(before: number): number;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { getSqliteDatabase, runSqliteWriteTransaction } from "../storage/sqlite/transaction.js";
|
|
2
|
+
import { ProductNotificationSchema } from "../../packages/gateway-contract/src/notifications.js";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
//#region src/notifications/store.ts
|
|
5
|
+
function parseJson(raw) {
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(raw);
|
|
8
|
+
} catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function eventFromRow(row) {
|
|
13
|
+
return ProductNotificationSchema.parse({
|
|
14
|
+
schemaVersion: 1,
|
|
15
|
+
id: row.event_id,
|
|
16
|
+
type: row.event_type,
|
|
17
|
+
target: parseJson(row.target_json),
|
|
18
|
+
priority: row.priority,
|
|
19
|
+
title: {
|
|
20
|
+
en: row.title_en,
|
|
21
|
+
zh: row.title_zh
|
|
22
|
+
},
|
|
23
|
+
...row.body_en && row.body_zh ? { body: {
|
|
24
|
+
en: row.body_en,
|
|
25
|
+
zh: row.body_zh
|
|
26
|
+
} } : {},
|
|
27
|
+
payload: parseJson(row.payload_json),
|
|
28
|
+
createdAt: row.created_at
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function deliveryFromRow(row) {
|
|
32
|
+
return {
|
|
33
|
+
event: eventFromRow(row),
|
|
34
|
+
deviceId: row.device_id,
|
|
35
|
+
pushToken: row.push_token,
|
|
36
|
+
locale: row.locale === "zh" ? "zh" : "en",
|
|
37
|
+
attempts: row.attempts,
|
|
38
|
+
...row.provider_ticket_id ? { providerTicketId: row.provider_ticket_id } : {}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function createNotificationEvent(input) {
|
|
42
|
+
const id = randomUUID();
|
|
43
|
+
const createdAt = input.createdAt ?? Date.now();
|
|
44
|
+
const created = runSqliteWriteTransaction((db) => {
|
|
45
|
+
const inserted = db.prepare(`INSERT OR IGNORE INTO notification_events (
|
|
46
|
+
event_id, dedupe_key, event_type, target_json, priority,
|
|
47
|
+
title_en, title_zh, body_en, body_zh, payload_json, created_at
|
|
48
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, input.dedupeKey, input.notification.type, JSON.stringify(input.notification.target), input.notification.priority, input.notification.title.en, input.notification.title.zh, input.notification.body?.en ?? null, input.notification.body?.zh ?? null, JSON.stringify(input.notification.payload), createdAt).changes > 0;
|
|
49
|
+
if (inserted) {
|
|
50
|
+
const enqueue = db.prepare(`INSERT INTO notification_deliveries (
|
|
51
|
+
event_id, device_id, status, next_attempt_at, updated_at
|
|
52
|
+
) VALUES (?, ?, 'pending', ?, ?)`);
|
|
53
|
+
for (const deviceId of input.deviceIds) enqueue.run(id, deviceId, createdAt, createdAt);
|
|
54
|
+
}
|
|
55
|
+
return inserted;
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
notification: eventFromRow(getSqliteDatabase().prepare("SELECT * FROM notification_events WHERE dedupe_key = ?").get(input.dedupeKey)),
|
|
59
|
+
created
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function listNotificationEvents(options = {}) {
|
|
63
|
+
const limit = Math.max(1, Math.min(100, options.limit ?? 50));
|
|
64
|
+
const after = options.afterId ? getSqliteDatabase().prepare("SELECT created_at, event_id FROM notification_events WHERE event_id = ?").get(options.afterId) : void 0;
|
|
65
|
+
const items = (after ? getSqliteDatabase().prepare(`SELECT * FROM notification_events
|
|
66
|
+
WHERE created_at > ? OR (created_at = ? AND event_id > ?)
|
|
67
|
+
ORDER BY created_at, event_id LIMIT ?`).all(after.created_at, after.created_at, after.event_id, limit) : getSqliteDatabase().prepare(`SELECT * FROM notification_events WHERE created_at >= ?
|
|
68
|
+
ORDER BY created_at, event_id LIMIT ?`).all(options.since ?? Date.now(), limit)).map(eventFromRow);
|
|
69
|
+
return {
|
|
70
|
+
items,
|
|
71
|
+
nextCursor: items.at(-1)?.id ?? null
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function acknowledgeNotification(eventId, consumerId, surface) {
|
|
75
|
+
if (!getSqliteDatabase().prepare("SELECT 1 FROM notification_events WHERE event_id = ?").get(eventId)) return false;
|
|
76
|
+
runSqliteWriteTransaction((db) => {
|
|
77
|
+
db.prepare(`INSERT INTO notification_acknowledgements (
|
|
78
|
+
event_id, consumer_id, surface, acknowledged_at
|
|
79
|
+
) VALUES (?, ?, ?, ?)
|
|
80
|
+
ON CONFLICT(event_id, consumer_id) DO UPDATE SET
|
|
81
|
+
surface = excluded.surface,
|
|
82
|
+
acknowledged_at = excluded.acknowledged_at`).run(eventId, consumerId, surface, Date.now());
|
|
83
|
+
});
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
const DELIVERY_JOIN = `
|
|
87
|
+
SELECT e.*, d.device_id, d.attempts, d.provider_ticket_id,
|
|
88
|
+
n.push_token, n.locale
|
|
89
|
+
FROM notification_deliveries d
|
|
90
|
+
JOIN notification_events e ON e.event_id = d.event_id
|
|
91
|
+
JOIN notification_devices n ON n.device_id = d.device_id
|
|
92
|
+
`;
|
|
93
|
+
function listDueNotificationDeliveries(status, now = Date.now(), limit = 100) {
|
|
94
|
+
return getSqliteDatabase().prepare(`${DELIVERY_JOIN}
|
|
95
|
+
WHERE d.status = ? AND d.next_attempt_at <= ?
|
|
96
|
+
AND n.enabled = 1 AND n.permissions = 'granted' AND n.lease_expires_at > ?
|
|
97
|
+
ORDER BY d.next_attempt_at LIMIT ?`).all(status, now, now, Math.max(1, Math.min(1e3, limit))).map(deliveryFromRow);
|
|
98
|
+
}
|
|
99
|
+
function markNotificationDeliveryAccepted(eventId, deviceId, ticketId, receiptAt) {
|
|
100
|
+
runSqliteWriteTransaction((db) => {
|
|
101
|
+
db.prepare(`UPDATE notification_deliveries
|
|
102
|
+
SET status = 'accepted', attempts = attempts + 1, provider_ticket_id = ?,
|
|
103
|
+
next_attempt_at = ?, last_error = NULL, updated_at = ?
|
|
104
|
+
WHERE event_id = ? AND device_id = ?`).run(ticketId, receiptAt, Date.now(), eventId, deviceId);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function markNotificationDeliveryDelivered(eventId, deviceId) {
|
|
108
|
+
runSqliteWriteTransaction((db) => {
|
|
109
|
+
db.prepare(`UPDATE notification_deliveries
|
|
110
|
+
SET status = 'delivered', last_error = NULL, updated_at = ?
|
|
111
|
+
WHERE event_id = ? AND device_id = ?`).run(Date.now(), eventId, deviceId);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function markNotificationDeliveryDead(eventId, deviceId, error) {
|
|
115
|
+
runSqliteWriteTransaction((db) => {
|
|
116
|
+
db.prepare(`UPDATE notification_deliveries
|
|
117
|
+
SET status = 'dead', attempts = attempts + 1, last_error = ?, updated_at = ?
|
|
118
|
+
WHERE event_id = ? AND device_id = ?`).run(error.slice(0, 500), Date.now(), eventId, deviceId);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
function rescheduleNotificationDelivery(eventId, deviceId, status, nextAttemptAt, error) {
|
|
122
|
+
runSqliteWriteTransaction((db) => {
|
|
123
|
+
db.prepare(`UPDATE notification_deliveries
|
|
124
|
+
SET status = ?, attempts = attempts + 1, next_attempt_at = ?, last_error = ?, updated_at = ?
|
|
125
|
+
WHERE event_id = ? AND device_id = ?`).run(status, nextAttemptAt, error.slice(0, 500), Date.now(), eventId, deviceId);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function notificationDeliveryMetrics() {
|
|
129
|
+
const rows = getSqliteDatabase().prepare("SELECT status, COUNT(*) AS count FROM notification_deliveries GROUP BY status").all();
|
|
130
|
+
const counts = Object.fromEntries(rows.map((row) => [row.status, row.count]));
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
const due = getSqliteDatabase().prepare(`SELECT COUNT(*) AS count FROM notification_deliveries
|
|
133
|
+
WHERE status IN ('pending', 'accepted') AND next_attempt_at <= ?`).get(now);
|
|
134
|
+
const devices = getSqliteDatabase().prepare(`SELECT
|
|
135
|
+
SUM(CASE WHEN enabled = 1 AND permissions = 'granted' AND lease_expires_at > ? THEN 1 ELSE 0 END) AS deliverable,
|
|
136
|
+
SUM(CASE WHEN enabled = 1 AND lease_expires_at <= ? THEN 1 ELSE 0 END) AS expired,
|
|
137
|
+
SUM(CASE WHEN enabled = 0 OR permissions <> 'granted' THEN 1 ELSE 0 END) AS disabled
|
|
138
|
+
FROM notification_devices`).get(now, now);
|
|
139
|
+
const oldest = getSqliteDatabase().prepare(`SELECT MIN(next_attempt_at) AS value FROM notification_deliveries
|
|
140
|
+
WHERE status IN ('pending', 'accepted')`).get();
|
|
141
|
+
const latestError = getSqliteDatabase().prepare(`SELECT event_id, device_id, last_error, updated_at
|
|
142
|
+
FROM notification_deliveries WHERE last_error IS NOT NULL
|
|
143
|
+
ORDER BY updated_at DESC LIMIT 1`).get();
|
|
144
|
+
return {
|
|
145
|
+
pending: counts.pending ?? 0,
|
|
146
|
+
accepted: counts.accepted ?? 0,
|
|
147
|
+
delivered: counts.delivered ?? 0,
|
|
148
|
+
dead: counts.dead ?? 0,
|
|
149
|
+
due: due.count,
|
|
150
|
+
devices: {
|
|
151
|
+
deliverable: devices.deliverable ?? 0,
|
|
152
|
+
expired: devices.expired ?? 0,
|
|
153
|
+
disabled: devices.disabled ?? 0
|
|
154
|
+
},
|
|
155
|
+
oldestPendingAt: oldest.value,
|
|
156
|
+
latestError: latestError ? {
|
|
157
|
+
eventId: latestError.event_id,
|
|
158
|
+
deviceId: latestError.device_id,
|
|
159
|
+
message: latestError.last_error,
|
|
160
|
+
updatedAt: latestError.updated_at
|
|
161
|
+
} : null
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function expireUndeliverableNotificationDeliveries(now = Date.now()) {
|
|
165
|
+
return Number(runSqliteWriteTransaction((db) => db.prepare(`UPDATE notification_deliveries
|
|
166
|
+
SET status = 'dead', last_error = 'Device registration is disabled or expired', updated_at = ?
|
|
167
|
+
WHERE status IN ('pending', 'accepted')
|
|
168
|
+
AND EXISTS (
|
|
169
|
+
SELECT 1 FROM notification_devices n
|
|
170
|
+
WHERE n.device_id = notification_deliveries.device_id
|
|
171
|
+
AND (n.enabled = 0 OR n.permissions <> 'granted' OR n.lease_expires_at <= ?)
|
|
172
|
+
)`).run(now, now).changes));
|
|
173
|
+
}
|
|
174
|
+
function pruneNotificationEvents(before) {
|
|
175
|
+
return Number(runSqliteWriteTransaction((db) => db.prepare(`DELETE FROM notification_events
|
|
176
|
+
WHERE created_at < ?
|
|
177
|
+
AND NOT EXISTS (
|
|
178
|
+
SELECT 1 FROM notification_deliveries d
|
|
179
|
+
WHERE d.event_id = notification_events.event_id
|
|
180
|
+
AND d.status IN ('pending', 'accepted')
|
|
181
|
+
)`).run(before).changes));
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
export { acknowledgeNotification, createNotificationEvent, expireUndeliverableNotificationDeliveries, listDueNotificationDeliveries, listNotificationEvents, markNotificationDeliveryAccepted, markNotificationDeliveryDead, markNotificationDeliveryDelivered, notificationDeliveryMetrics, pruneNotificationEvents, rescheduleNotificationDelivery };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type NotificationDevicePlatform = 'ios' | 'android';
|
|
2
|
+
export type NotificationPermission = 'granted' | 'denied' | 'unknown';
|
|
3
|
+
export type NotificationLanguage = 'en' | 'zh';
|
|
4
|
+
export type NotificationPreferences = {
|
|
5
|
+
chatCompleted: boolean;
|
|
6
|
+
chatFailed: boolean;
|
|
7
|
+
taskNeedsInput: boolean;
|
|
8
|
+
taskBlocked: boolean;
|
|
9
|
+
taskFailed: boolean;
|
|
10
|
+
taskCompleted: boolean;
|
|
11
|
+
automationCompleted: boolean;
|
|
12
|
+
automationFailed: boolean;
|
|
13
|
+
proactiveInsight: boolean;
|
|
14
|
+
};
|
|
15
|
+
export type NotificationDevice = {
|
|
16
|
+
id: string;
|
|
17
|
+
platform: NotificationDevicePlatform;
|
|
18
|
+
pushToken: string;
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
permissions: NotificationPermission;
|
|
21
|
+
preferences: NotificationPreferences;
|
|
22
|
+
locale: NotificationLanguage;
|
|
23
|
+
appVersion?: string;
|
|
24
|
+
leaseExpiresAt: number;
|
|
25
|
+
lastSeenAt: number;
|
|
26
|
+
createdAt: number;
|
|
27
|
+
updatedAt: number;
|
|
28
|
+
};
|
|
29
|
+
export declare const DEFAULT_NOTIFICATION_PREFERENCES: NotificationPreferences;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/notifications/types.ts
|
|
2
|
+
const DEFAULT_NOTIFICATION_PREFERENCES = {
|
|
3
|
+
chatCompleted: true,
|
|
4
|
+
chatFailed: true,
|
|
5
|
+
taskNeedsInput: true,
|
|
6
|
+
taskBlocked: true,
|
|
7
|
+
taskFailed: true,
|
|
8
|
+
taskCompleted: false,
|
|
9
|
+
automationCompleted: false,
|
|
10
|
+
automationFailed: true,
|
|
11
|
+
proactiveInsight: true
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
export { DEFAULT_NOTIFICATION_PREFERENCES };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TurnOutcome } from '@xopcai/gateway-contract';
|
|
1
2
|
import type { Message } from './types.js';
|
|
2
3
|
import type { TranscriptStoredRow } from './session-context-for-llm.js';
|
|
3
4
|
/** Transcript row for TUI and HTTP clients (flattened from persisted session messages). */
|
|
@@ -13,7 +14,7 @@ export interface ClientHistoryMessage {
|
|
|
13
14
|
timestamp?: number;
|
|
14
15
|
/** Whitelisted display metadata; never includes source snapshot text. */
|
|
15
16
|
metadata?: {
|
|
16
|
-
sourceContexts
|
|
17
|
+
sourceContexts?: Array<{
|
|
17
18
|
kind: 'note';
|
|
18
19
|
sourceId: string;
|
|
19
20
|
version: string;
|
|
@@ -21,6 +22,7 @@ export interface ClientHistoryMessage {
|
|
|
21
22
|
tokenEstimate?: number;
|
|
22
23
|
truncated?: boolean;
|
|
23
24
|
}>;
|
|
25
|
+
turnOutcome?: TurnOutcome;
|
|
24
26
|
};
|
|
25
27
|
kind?: 'message' | 'compaction' | 'context' | 'bash' | 'custom' | 'branch';
|
|
26
28
|
tokensBefore?: number;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseTurnOutcome } from "../../packages/gateway-contract/src/turn-outcome.js";
|
|
1
2
|
import { stripRuntimeContextFromUserMessage } from "./user-message-display.js";
|
|
2
3
|
import { buildTranscriptOutline } from "./transcript-outline.js";
|
|
3
4
|
//#region src/session/client-history.ts
|
|
@@ -286,6 +287,18 @@ function customStateRowToClientHistory(row) {
|
|
|
286
287
|
if (r.type !== "custom") return null;
|
|
287
288
|
const customType = optionalString(r.customType)?.trim();
|
|
288
289
|
if (!customType) return null;
|
|
290
|
+
if (customType === "turn_outcome") {
|
|
291
|
+
const outcome = parseTurnOutcome(r.data);
|
|
292
|
+
if (!outcome) return null;
|
|
293
|
+
return {
|
|
294
|
+
role: "assistant",
|
|
295
|
+
kind: "message",
|
|
296
|
+
turnId: outcome.turnId,
|
|
297
|
+
content: "",
|
|
298
|
+
timestamp: parseTimestampValue(typeof r.timestamp === "string" || typeof r.timestamp === "number" ? r.timestamp : void 0),
|
|
299
|
+
metadata: { turnOutcome: outcome }
|
|
300
|
+
};
|
|
301
|
+
}
|
|
289
302
|
return {
|
|
290
303
|
role: "system",
|
|
291
304
|
kind: "custom",
|