@xopcai/xopc 0.0.268 → 0.0.269
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-Bhyy6ENz.js → activity-store-DcwCSY6s.js} +1 -1
- package/dist/gateway/static/root/assets/{agent-avatar-dicebear-B9WObxjr.js → agent-avatar-dicebear-D0NBOWC4.js} +1 -1
- package/dist/gateway/static/root/assets/{agent-defaults-settings-panel-J1YFNG26.js → agent-defaults-settings-panel-qKCezlBg.js} +1 -1
- package/dist/gateway/static/root/assets/agents-BMhnZKhP.js +1 -0
- package/dist/gateway/static/root/assets/{api-B2y7ZjZk.js → api-eaPO-mdr.js} +1 -1
- package/dist/gateway/static/root/assets/{app-management-settings-panel-Bbn9mDfL.js → app-management-settings-panel-E3xm2teY.js} +1 -1
- package/dist/gateway/static/root/assets/{appearance-settings-Deo5EsIc.js → appearance-settings-CiZOjgls.js} +1 -1
- package/dist/gateway/static/root/assets/apps-page-DduJDWGY.js +1 -0
- package/dist/gateway/static/root/assets/{archive-plugin-CxJTU8MU.js → archive-plugin-D09yo_Hc.js} +1 -1
- package/dist/gateway/static/root/assets/{attachment-load-C71Cukgp.js → attachment-load-CNYb8Qru.js} +1 -1
- package/dist/gateway/static/root/assets/{automations-page-_J7UZ55Q.js → automations-page-DsrfW5ii.js} +1 -1
- package/dist/gateway/static/root/assets/automations-workspace-X7hNHCjW.js +6 -0
- package/dist/gateway/static/root/assets/{binary-plugins-CX8fC8Zt.js → binary-plugins-OWlaGqlY.js} +1 -1
- package/dist/gateway/static/root/assets/{block-editor-Ci9fjoQL.js → block-editor-CVND3M6r.js} +3 -3
- package/dist/gateway/static/root/assets/{browser-automation-inputs-s5I78Iko.js → browser-automation-inputs-D7IzVK_J.js} +1 -1
- package/dist/gateway/static/root/assets/{browser-automations-page-rDSx0ozx.js → browser-automations-page-CQ6cCpIv.js} +2 -2
- package/dist/gateway/static/root/assets/browser-settings-page-BZ7nK_pN.js +8 -0
- package/dist/gateway/static/root/assets/capabilities-settings-panel-BfKvJO1C.js +7 -0
- package/dist/gateway/static/root/assets/channels-settings-D1UDxJuA.js +1 -0
- package/dist/gateway/static/root/assets/{channels-status-swr-_OsWr_vS.js → channels-status-swr-D4JrgJKa.js} +1 -1
- package/dist/gateway/static/root/assets/{chat-terminal-dock-DMSl8KxS.js → chat-terminal-dock-BUjnhT7C.js} +2 -2
- package/dist/gateway/static/root/assets/connectors-page-C2nuqs_O.js +2 -0
- package/dist/gateway/static/root/assets/{cron-expression-BByq6Uh8.js → cron-expression-DlyRIyUn.js} +1 -1
- package/dist/gateway/static/root/assets/{date-picker-m6JZCUEW.js → date-picker-CU-ij_yE.js} +1 -1
- package/dist/gateway/static/root/assets/{dependency-picker-CwCAjbaJ.js → dependency-picker-Z6Nf0Wjq.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-DSmeEREd.js → desktop-pet-C0F5Iqnc.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-settings-MfKceMoe.js → desktop-pet-settings-Bj_W6HL1.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-CwvWy26h.js → extension-debug-page-Ba09EnAn.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-DLrbVBHM.js → extension-page-C88aGXJs.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-BI6C4PM-.js → extension-settings-page-CMyt0A32.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-config-swr-OObCK8jM.js → gateway-config-swr-bjzk_4hn.js} +1 -1
- package/dist/gateway/static/root/assets/gateway-settings-ogv8ZbvG.js +1 -0
- package/dist/gateway/static/root/assets/{gateway-startup-retry-D_TxKN_A.js → gateway-startup-retry-DrbIzFyB.js} +1 -1
- package/dist/gateway/static/root/assets/{heartbeat-settings-CUl832Tb.js → heartbeat-settings-BnrMRKaZ.js} +1 -1
- package/dist/gateway/static/root/assets/home-page-CbcJCREd.js +1 -0
- package/dist/gateway/static/root/assets/{image-generation-api-fPU9yGsw.js → image-generation-api-CBWU9Ner.js} +1 -1
- package/dist/gateway/static/root/assets/index-7PZ6jmhy.css +2 -0
- package/dist/gateway/static/root/assets/{index-wKE8Q2FX.js → index-CfyyA9-g.js} +47 -47
- package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-r4DCS19O.js → keyboard-shortcuts-settings-B8Z7kKQY.js} +1 -1
- package/dist/gateway/static/root/assets/local-app-workbench-page-D5XVZnK1.js +2 -0
- package/dist/gateway/static/root/assets/{local-apps-page-DkfCRLUh.js → local-apps-page-CLSfzkRp.js} +1 -1
- package/dist/gateway/static/root/assets/{locale-store-DFQX9kTl.js → locale-store-BQ2HKTWb.js} +1 -1
- package/dist/gateway/static/root/assets/logs-page-CWDU1AGN.js +2 -0
- package/dist/gateway/static/root/assets/{management-settings-Cfx4ATTa.js → management-settings-CuFNgQUy.js} +1 -1
- package/dist/gateway/static/root/assets/{markdown-view-D-5jHRWg.js → markdown-view-DIcfrLmG.js} +1 -1
- package/dist/gateway/static/root/assets/{media-plugins-DOLjWd3y.js → media-plugins-CSv6-xTA.js} +1 -1
- package/dist/gateway/static/root/assets/{messages-Dj8QuS6e.js → messages-BRjoarIr.js} +2 -2
- package/dist/gateway/static/root/assets/{mobile-pairing-wizard-BGW9joAb.js → mobile-pairing-wizard-C7d_ztgp.js} +1 -1
- package/dist/gateway/static/root/assets/note-detail-panel-D5JuBLyt.js +5 -0
- package/dist/gateway/static/root/assets/notes-page-C5-ccCi4.js +8 -0
- package/dist/gateway/static/root/assets/{page-tabs-_SGXMT2O.js → page-tabs-BfV-PQa5.js} +1 -1
- package/dist/gateway/static/root/assets/{preview-open-alternatives-lVetfCWk.js → preview-open-alternatives-DQS7NFpg.js} +1 -1
- package/dist/gateway/static/root/assets/proactive-page-fHjPigmx.js +1 -0
- package/dist/gateway/static/root/assets/{product-open-page-CIGLPFm7.js → product-open-page-BIhjZLKg.js} +1 -1
- package/dist/gateway/static/root/assets/project-detail-page-6v_khvEt.js +1 -0
- package/dist/gateway/static/root/assets/projects-page-CCy1aK4V.js +1 -0
- package/dist/gateway/static/root/assets/{registry-api-CsZLatKo.js → registry-api-CwveUfe8.js} +1 -1
- package/dist/gateway/static/root/assets/remote-access-hub-pkqKRnch.js +1 -0
- package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-BDqVrUEj.js → runtime-tools-settings-panel-DMQ0wpNg.js} +2 -2
- package/dist/gateway/static/root/assets/{schema-form-8RQgFJJO.js → schema-form-C91g2PRU.js} +1 -1
- package/dist/gateway/static/root/assets/sessions-page-Cr4E4cap.js +1 -0
- package/dist/gateway/static/root/assets/settings-advanced-gate-D1JRDRyL.js +1 -0
- package/dist/gateway/static/root/assets/{settings-form-section-j4UjGM-f.js → settings-form-section-Cem6L49Y.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-page-lMi50B_s.js → settings-page-afDUxqlV.js} +1 -1
- package/dist/gateway/static/root/assets/{setup-status-panel-Dx4Fy3rU.js → setup-status-panel-Cc_iP_vv.js} +1 -1
- package/dist/gateway/static/root/assets/{share-preview-page-aSyKRH5y.js → share-preview-page-CVEVHksg.js} +1 -1
- package/dist/gateway/static/root/assets/shares-settings-BrMECMAB.js +1 -0
- package/dist/gateway/static/root/assets/skill-reload-api-DuqIDYjc.js +1 -0
- package/dist/gateway/static/root/assets/{skills-page-C2_CAvxW.js → skills-page-CNrq1W6C.js} +1 -1
- package/dist/gateway/static/root/assets/{skills-page.utils-B00e4Q30.js → skills-page.utils-D1wGfm22.js} +1 -1
- package/dist/gateway/static/root/assets/{system-settings-panel-CulAkrsV.js → system-settings-panel-BLE8PxWE.js} +1 -1
- package/dist/gateway/static/root/assets/task-detail-page-m0oXUKl2.js +1 -0
- package/dist/gateway/static/root/assets/{theme-store-zRT6vaZc.js → theme-store-BejLVjIT.js} +1 -1
- package/dist/gateway/static/root/assets/{tunnel-api-C01xgCWR.js → tunnel-api-Dur80zSQ.js} +1 -1
- package/dist/gateway/static/root/assets/{use-autosave-DfSJ3c2p.js → use-autosave-DmgEA8Qb.js} +1 -1
- package/dist/gateway/static/root/assets/user-model-page-Dl10D_5q.js +2 -0
- package/dist/gateway/static/root/assets/{voice-api-key-field-9v-miN8o.js → voice-api-key-field-sZ0J83SF.js} +1 -1
- package/dist/gateway/static/root/assets/{voice-config-api-DpJEu2RQ.js → voice-config-api-BEbz3avx.js} +1 -1
- package/dist/gateway/static/root/assets/{work-discovery-overlay-CREYgh-1.js → work-discovery-overlay-BXnAlbmO.js} +1 -1
- package/dist/gateway/static/root/assets/workflow-run-setup-panel-BkwFey0a.js +3 -0
- package/dist/gateway/static/root/assets/workflows-page-CswgoIFU.js +6 -0
- package/dist/gateway/static/root/index.html +10 -10
- package/dist/gateway/static/root/notification-sw.js +15 -0
- package/dist/package.js +1 -1
- package/dist/packages/gateway-contract/src/notifications.js +15 -1
- package/dist/packages/gateway-contract/src/proactive.js +104 -0
- package/dist/packages/realtime-protocol/src/voice.js +2 -1
- package/dist/src/agent/workflow/subagent-runner.d.ts +1 -0
- package/dist/src/agent/workflow/subagent-runner.js +2 -2
- package/dist/src/cli/commands/connectors.js +1 -1
- package/dist/src/cli/commands/doctor/checks/database-schema.js +1 -1
- package/dist/src/connectors/connected-source-ingestion.js +1 -1
- package/dist/src/connectors/connection-recovery-service.js +1 -1
- package/dist/src/connectors/health.js +1 -1
- package/dist/src/gateway/hono/routes/capabilities.js +1 -1
- package/dist/src/gateway/hono/routes/connectors.js +1 -1
- package/dist/src/gateway/hono/routes/index.js +0 -2
- package/dist/src/gateway/hono/routes/lazy-bundles.js +12 -0
- package/dist/src/gateway/hono/routes/mcp.js +1 -1
- package/dist/src/gateway/hono/routes/proactive-control.d.ts +3 -0
- package/dist/src/gateway/hono/routes/proactive-control.js +161 -0
- package/dist/src/gateway/hono/routes/proactive.d.ts +1 -1
- package/dist/src/gateway/hono/routes/proactive.js +12 -5
- package/dist/src/gateway/hono/routes/voice.js +1 -1
- package/dist/src/gateway/security/gateway-scopes.js +5 -0
- package/dist/src/gateway/service.d.ts +0 -1
- package/dist/src/gateway/service.js +24 -26
- package/dist/src/notifications/planner.js +4 -3
- package/dist/src/notifications/proactive-channel.d.ts +8 -0
- package/dist/src/notifications/proactive-channel.js +57 -0
- package/dist/src/notifications/proactive-policy.d.ts +3 -0
- package/dist/src/notifications/proactive-policy.js +32 -0
- package/dist/src/notifications/service.d.ts +78 -0
- package/dist/src/notifications/service.js +71 -13
- package/dist/src/notifications/store.d.ts +2 -0
- package/dist/src/notifications/store.js +8 -1
- package/dist/src/notifications/web-push.d.ts +19 -0
- package/dist/src/notifications/web-push.js +182 -0
- package/dist/src/proactive/actions/service.js +10 -2
- package/dist/src/proactive/actions/workflow.d.ts +15 -0
- package/dist/src/proactive/actions/workflow.js +88 -0
- package/dist/src/proactive/execution/agent-executor.d.ts +5 -0
- package/dist/src/proactive/execution/agent-executor.js +21 -0
- package/dist/src/proactive/execution/authorization.d.ts +3 -0
- package/dist/src/proactive/execution/authorization.js +23 -0
- package/dist/src/proactive/execution/context.js +2 -11
- package/dist/src/proactive/execution/insight.d.ts +7 -0
- package/dist/src/proactive/execution/insight.js +33 -2
- package/dist/src/proactive/execution/repository.d.ts +2 -0
- package/dist/src/proactive/execution/repository.js +38 -4
- package/dist/src/proactive/execution/types.d.ts +5 -0
- package/dist/src/proactive/execution/worker.js +38 -7
- package/dist/src/proactive/inbox/cards.d.ts +21 -0
- package/dist/src/proactive/inbox/cards.js +237 -0
- package/dist/src/proactive/inbox/delivery.d.ts +6 -0
- package/dist/src/proactive/inbox/delivery.js +52 -0
- package/dist/src/proactive/inbox/digest.d.ts +8 -0
- package/dist/src/proactive/inbox/digest.js +112 -0
- package/dist/src/proactive/inbox/lifecycle.d.ts +4 -0
- package/dist/src/proactive/inbox/lifecycle.js +58 -0
- package/dist/src/proactive/inbox/notification-policy.d.ts +1 -0
- package/dist/src/proactive/inbox/notification-policy.js +20 -0
- package/dist/src/proactive/inbox/repository.d.ts +1 -0
- package/dist/src/proactive/inbox/repository.js +68 -11
- package/dist/src/proactive/inbox/types.d.ts +9 -1
- package/dist/src/proactive/inbox/worker.js +9 -3
- package/dist/src/proactive/index.js +1 -1
- package/dist/src/proactive/maintenance.js +7 -0
- package/dist/src/proactive/metrics.d.ts +10 -0
- package/dist/src/proactive/metrics.js +27 -0
- package/dist/src/proactive/policy/presence.d.ts +2 -0
- package/dist/src/proactive/policy/presence.js +27 -0
- package/dist/src/proactive/policy/service.d.ts +53 -0
- package/dist/src/proactive/policy/service.js +122 -0
- package/dist/src/proactive/routing/matcher.js +3 -2
- package/dist/src/proactive/scenarios/control.d.ts +90 -0
- package/dist/src/proactive/scenarios/control.js +109 -0
- package/dist/src/proactive/scenarios/preview.d.ts +13 -0
- package/dist/src/proactive/scenarios/preview.js +81 -0
- package/dist/src/proactive/scenarios/prompt-composer.js +2 -2
- package/dist/src/proactive/scenarios/repository.js +5 -1
- package/dist/src/proactive/temporal/schedule.d.ts +3 -0
- package/dist/src/proactive/temporal/schedule.js +67 -0
- package/dist/src/proactive/temporal/worker.js +8 -4
- package/dist/src/storage/sqlite/migrations/159_proactive_controls.sql +68 -0
- package/dist/src/storage/sqlite/migrations/160_proactive_web_push.sql +27 -0
- package/dist/src/storage/sqlite/migrations/161_proactive_delivery_and_lifecycle.sql +51 -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/tui/tui-startup-resources.js +1 -1
- package/dist/src/voice/realtime/runtime.d.ts +2 -1
- package/dist/src/voice/realtime/runtime.js +21 -5
- package/dist/src/workflows/domain/run.d.ts +1 -0
- package/dist/src/workflows/index.js +2 -2
- package/dist/src/workflows/service/workflow-run-service.d.ts +3 -0
- package/dist/src/workflows/service/workflow-run-service.js +26 -5
- package/dist/src/workflows/service/workflow-run-service.types.d.ts +1 -0
- package/package.json +6 -4
- package/dist/gateway/static/root/assets/agents-BJuTIRPD.js +0 -1
- package/dist/gateway/static/root/assets/apps-page-Cz4d7ErX.js +0 -1
- package/dist/gateway/static/root/assets/automations-workspace-Bn8nwUOL.js +0 -6
- package/dist/gateway/static/root/assets/browser-settings-page-B9RA6x4x.js +0 -8
- package/dist/gateway/static/root/assets/capabilities-settings-panel-C1GUih8m.js +0 -7
- package/dist/gateway/static/root/assets/channels-settings-CEhVLJNa.js +0 -1
- package/dist/gateway/static/root/assets/connectors-page-IQAQDSmO.js +0 -2
- package/dist/gateway/static/root/assets/gateway-settings-B3H7Vfbh.js +0 -1
- package/dist/gateway/static/root/assets/home-page-CT4JL2jm.js +0 -1
- package/dist/gateway/static/root/assets/index-CRJrO4GI.css +0 -2
- package/dist/gateway/static/root/assets/local-app-workbench-page-qwLuir8A.js +0 -2
- package/dist/gateway/static/root/assets/logs-page-DrNaaPWc.js +0 -2
- package/dist/gateway/static/root/assets/note-detail-panel-Dbj5OvXE.js +0 -5
- package/dist/gateway/static/root/assets/notes-page-5hRB3WXc.js +0 -8
- package/dist/gateway/static/root/assets/project-detail-page-CblWLA8k.js +0 -1
- package/dist/gateway/static/root/assets/projects-page-CGk-kzqs.js +0 -1
- package/dist/gateway/static/root/assets/remote-access-hub-Du8PweyU.js +0 -1
- package/dist/gateway/static/root/assets/sessions-page-BFm94dJC.js +0 -1
- package/dist/gateway/static/root/assets/settings-advanced-gate-C0H-cToA.js +0 -1
- package/dist/gateway/static/root/assets/shares-settings-CqF9s0pl.js +0 -1
- package/dist/gateway/static/root/assets/skill-reload-api-BBL_ZUSC.js +0 -1
- package/dist/gateway/static/root/assets/task-detail-page-D1FmRFIn.js +0 -1
- package/dist/gateway/static/root/assets/user-model-page-BGC_5baJ.js +0 -2
- package/dist/gateway/static/root/assets/workflow-run-setup-panel-BfCWdw8F.js +0 -3
- package/dist/gateway/static/root/assets/workflows-page-CvmKQkD0.js +0 -6
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { getSqliteDatabase, runSqliteWriteTransaction } from "../../storage/sqlite/transaction.js";
|
|
2
|
+
import { ProactiveCardActionSchema } from "../../../packages/gateway-contract/src/proactive.js";
|
|
3
|
+
import { getKnowledgeSourceItem } from "../../storage/sqlite/knowledge-repository.js";
|
|
4
|
+
import { ProactiveConflict, effectiveProactivePolicy, subscriptionSettings } from "../policy/service.js";
|
|
5
|
+
import { insightSourcesAuthorized } from "../execution/authorization.js";
|
|
6
|
+
import { withdrawCard } from "./lifecycle.js";
|
|
7
|
+
import { executePendingProactiveActions } from "../actions/service.js";
|
|
8
|
+
import { updateControlledSubscription } from "../scenarios/control.js";
|
|
9
|
+
import { getInboxItem } from "./repository.js";
|
|
10
|
+
import { ProactiveInboxService } from "./service.js";
|
|
11
|
+
//#region src/proactive/inbox/cards.ts
|
|
12
|
+
function requireCardScope(id, workspaceId) {
|
|
13
|
+
if (!getSqliteDatabase().prepare(`SELECT 1 FROM proactive_inbox_items i JOIN proactive_insights x USING(insight_id)
|
|
14
|
+
JOIN proactive_scenario_subscriptions s USING(subscription_id) WHERE i.inbox_item_id = ? AND s.workspace_id = ?`).get(id, workspaceId)) throw new Error("Card not found");
|
|
15
|
+
}
|
|
16
|
+
function getCard(id, workspaceId) {
|
|
17
|
+
requireCardScope(id, workspaceId);
|
|
18
|
+
const item = getInboxItem(id);
|
|
19
|
+
if (!insightSourcesAuthorized(item.insightId)) {
|
|
20
|
+
withdrawCard(id);
|
|
21
|
+
item.withdrawnAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
22
|
+
}
|
|
23
|
+
if (item.withdrawnAt) return {
|
|
24
|
+
schemaVersion: 1,
|
|
25
|
+
id,
|
|
26
|
+
revision: getInboxItem(id).revision,
|
|
27
|
+
notificationRevision: item.notificationRevision,
|
|
28
|
+
subscriptionId: item.subscriptionId,
|
|
29
|
+
scenarioKey: item.insight.scenarioKey,
|
|
30
|
+
kind: "briefing",
|
|
31
|
+
status: "withdrawn",
|
|
32
|
+
title: "Source unavailable / 来源已撤回",
|
|
33
|
+
summary: "",
|
|
34
|
+
whyNow: "",
|
|
35
|
+
recommendation: "",
|
|
36
|
+
workDone: "",
|
|
37
|
+
evidence: [],
|
|
38
|
+
createdAt: item.createdAt,
|
|
39
|
+
updatedAt: item.withdrawnAt,
|
|
40
|
+
fallbackText: "Source unavailable / 来源已撤回"
|
|
41
|
+
};
|
|
42
|
+
const insight = item.insight;
|
|
43
|
+
const expired = item.expiresAt && Date.parse(item.expiresAt) <= Date.now();
|
|
44
|
+
return {
|
|
45
|
+
schemaVersion: 1,
|
|
46
|
+
id,
|
|
47
|
+
revision: item.revision,
|
|
48
|
+
notificationRevision: item.notificationRevision,
|
|
49
|
+
subscriptionId: item.subscriptionId,
|
|
50
|
+
preparationAvailable: Boolean(subscriptionSettings(item.subscriptionId).preparationWorkflowId),
|
|
51
|
+
relatedCardIds: item.correlationKey ? getSqliteDatabase().prepare("SELECT inbox_item_id AS id FROM proactive_inbox_items WHERE correlation_key = ? AND inbox_item_id <> ? AND withdrawn_at IS NULL").all(item.correlationKey, id).map((row) => row.id) : [],
|
|
52
|
+
scenarioKey: insight.scenarioKey,
|
|
53
|
+
kind: insight.attentionKind === "receipt" ? "receipt" : insight.decision ? "decision" : insight.scenarioKey === "meeting_preparation" ? "briefing" : insight.scenarioKey.includes("risk") || insight.scenarioKey.includes("failure") ? "risk" : insight.scenarioKey === "blocked_work" ? "reminder" : "recommendation",
|
|
54
|
+
status: expired ? "expired" : item.status,
|
|
55
|
+
title: insight.title,
|
|
56
|
+
summary: insight.summary,
|
|
57
|
+
whyNow: insight.whyNow,
|
|
58
|
+
recommendation: insight.recommendation,
|
|
59
|
+
workDone: insight.workDone,
|
|
60
|
+
evidence: insight.evidenceIds.map((evidenceId) => cardEvidence(evidenceId, workspaceId)),
|
|
61
|
+
...insight.decision ? { decision: insight.decision } : {},
|
|
62
|
+
actionStatus: insight.actionStatus,
|
|
63
|
+
actionResult: insight.actionResult,
|
|
64
|
+
actionError: insight.actionError,
|
|
65
|
+
createdAt: item.createdAt,
|
|
66
|
+
updatedAt: item.updatedAt,
|
|
67
|
+
expiresAt: item.expiresAt,
|
|
68
|
+
fallbackText: `${insight.title}\n${insight.summary}\n${insight.whyNow}`
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function listCards(workspaceId, input = {}) {
|
|
72
|
+
const limit = Math.min(100, Math.max(1, input.limit ?? 30));
|
|
73
|
+
const rows = getSqliteDatabase().prepare(`SELECT i.inbox_item_id, i.created_at FROM proactive_inbox_items i
|
|
74
|
+
JOIN proactive_insights x USING(insight_id) JOIN proactive_scenario_subscriptions s USING(subscription_id)
|
|
75
|
+
WHERE i.withdrawn_at IS NULL AND s.workspace_id = ? AND (? IS NULL OR i.status = ?) AND (? IS NULL OR i.created_at || ':' || i.inbox_item_id < ?)
|
|
76
|
+
ORDER BY i.created_at DESC, i.inbox_item_id DESC LIMIT ?`).all(workspaceId, input.status || null, input.status || null, input.before || null, input.before || null, limit + 1);
|
|
77
|
+
const page = rows.slice(0, limit);
|
|
78
|
+
const last = page.at(-1);
|
|
79
|
+
return {
|
|
80
|
+
cards: page.flatMap((row) => {
|
|
81
|
+
try {
|
|
82
|
+
const card = getCard(row.inbox_item_id, workspaceId);
|
|
83
|
+
return card.status === "withdrawn" ? [] : [card];
|
|
84
|
+
} catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}),
|
|
88
|
+
nextCursor: rows.length > limit && last ? `${last.created_at}:${last.inbox_item_id}` : null
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function cardChanges(workspaceId, cursor = 0) {
|
|
92
|
+
const rows = getSqliteDatabase().prepare("SELECT sequence, inbox_item_id, deleted FROM proactive_card_changes WHERE sequence > ? AND workspace_id = ? ORDER BY sequence LIMIT 200").all(cursor, workspaceId);
|
|
93
|
+
const items = [];
|
|
94
|
+
for (const row of rows) {
|
|
95
|
+
if (row.deleted) {
|
|
96
|
+
items.push({
|
|
97
|
+
id: row.inbox_item_id,
|
|
98
|
+
deleted: true
|
|
99
|
+
});
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const card = getCard(row.inbox_item_id, workspaceId);
|
|
104
|
+
items.push(card.status === "withdrawn" ? {
|
|
105
|
+
id: row.inbox_item_id,
|
|
106
|
+
deleted: true
|
|
107
|
+
} : {
|
|
108
|
+
id: row.inbox_item_id,
|
|
109
|
+
deleted: false,
|
|
110
|
+
card
|
|
111
|
+
});
|
|
112
|
+
} catch {
|
|
113
|
+
items.push({
|
|
114
|
+
id: row.inbox_item_id,
|
|
115
|
+
deleted: true
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
items,
|
|
121
|
+
nextCursor: rows.at(-1)?.sequence ?? cursor,
|
|
122
|
+
hasMore: rows.length === 200
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function performCardAction(id, workspaceId, value) {
|
|
126
|
+
const input = ProactiveCardActionSchema.parse(value);
|
|
127
|
+
return runSqliteWriteTransaction((db) => {
|
|
128
|
+
requireCardScope(id, workspaceId);
|
|
129
|
+
const request = JSON.stringify({
|
|
130
|
+
id,
|
|
131
|
+
...input
|
|
132
|
+
});
|
|
133
|
+
const prior = db.prepare("SELECT request_json, response_json FROM proactive_card_actions WHERE idempotency_key = ?").get(input.idempotencyKey);
|
|
134
|
+
if (prior) {
|
|
135
|
+
if (prior.request_json !== request) throw new ProactiveConflict("Idempotency key was already used for a different action");
|
|
136
|
+
return getCard(id, workspaceId);
|
|
137
|
+
}
|
|
138
|
+
const card = getCard(id, workspaceId);
|
|
139
|
+
if (card.revision !== input.expectedRevision) throw new ProactiveConflict("Card changed; refresh before acting");
|
|
140
|
+
if (card.status === "withdrawn" || card.status === "expired" || card.status === "resolved" && ![
|
|
141
|
+
"useful",
|
|
142
|
+
"not_useful",
|
|
143
|
+
"retry"
|
|
144
|
+
].includes(input.actionId)) throw new ProactiveConflict("Card is no longer actionable");
|
|
145
|
+
applyCardAction(card, workspaceId, input);
|
|
146
|
+
const result = getCard(id, workspaceId);
|
|
147
|
+
db.prepare("INSERT INTO proactive_card_actions(idempotency_key, inbox_item_id, request_json, response_json) VALUES (?, ?, ?, ?)").run(input.idempotencyKey, id, request, JSON.stringify(result));
|
|
148
|
+
return result;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function applyCardAction(card, workspaceId, input) {
|
|
152
|
+
const service = new ProactiveInboxService();
|
|
153
|
+
switch (input.actionId) {
|
|
154
|
+
case "retry":
|
|
155
|
+
if (!effectiveProactivePolicy(card.subscriptionId).enabled) throw new Error("Subscription paused");
|
|
156
|
+
if (!getSqliteDatabase().prepare("UPDATE proactive_insights SET action_status = 'pending', action_error = NULL, action_updated_at = ? WHERE insight_id = (SELECT insight_id FROM proactive_inbox_items WHERE inbox_item_id = ?) AND action_status = 'failed'").run((/* @__PURE__ */ new Date()).toISOString(), card.id).changes) throw new Error("Only failed actions can be retried");
|
|
157
|
+
executePendingProactiveActions();
|
|
158
|
+
break;
|
|
159
|
+
case "useful":
|
|
160
|
+
case "not_useful":
|
|
161
|
+
service.feedback(card.id, input.actionId);
|
|
162
|
+
break;
|
|
163
|
+
case "read":
|
|
164
|
+
service.transition(card.id, { status: "read" });
|
|
165
|
+
break;
|
|
166
|
+
case "resolve":
|
|
167
|
+
service.transition(card.id, {
|
|
168
|
+
status: "resolved",
|
|
169
|
+
resolution: "dismissed"
|
|
170
|
+
});
|
|
171
|
+
break;
|
|
172
|
+
case "snooze":
|
|
173
|
+
service.transition(card.id, {
|
|
174
|
+
status: "snoozed",
|
|
175
|
+
snoozedUntil: input.snoozedUntil ?? new Date(Date.now() + 36e5).toISOString()
|
|
176
|
+
});
|
|
177
|
+
break;
|
|
178
|
+
case "decide":
|
|
179
|
+
if (!input.choice) throw new Error("Decision choice required");
|
|
180
|
+
if (!effectiveProactivePolicy(card.subscriptionId).enabled) throw new Error("Subscription paused");
|
|
181
|
+
service.decide(card.id, input.choice);
|
|
182
|
+
break;
|
|
183
|
+
case "less":
|
|
184
|
+
case "pause": {
|
|
185
|
+
const settings = subscriptionSettings(card.subscriptionId);
|
|
186
|
+
updateControlledSubscription(workspaceId, card.subscriptionId, {
|
|
187
|
+
expectedRevision: settings.revision,
|
|
188
|
+
...input.actionId === "less" ? { delivery: "inbox" } : { enabled: false }
|
|
189
|
+
});
|
|
190
|
+
service.transition(card.id, {
|
|
191
|
+
status: "resolved",
|
|
192
|
+
resolution: input.actionId
|
|
193
|
+
});
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function cardEvidence(id, workspaceId) {
|
|
199
|
+
const db = getSqliteDatabase();
|
|
200
|
+
const split = id.indexOf(":");
|
|
201
|
+
const kind = id.slice(0, split);
|
|
202
|
+
const objectId = id.slice(split + 1);
|
|
203
|
+
if (kind === "task") {
|
|
204
|
+
const task = db.prepare("SELECT title FROM tasks WHERE task_id = ?").get(objectId);
|
|
205
|
+
if (task) return {
|
|
206
|
+
id,
|
|
207
|
+
label: task.title,
|
|
208
|
+
route: `/tasks/${encodeURIComponent(objectId)}`
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (kind === "project") {
|
|
212
|
+
const project = db.prepare("SELECT name FROM projects WHERE project_id = ?").get(objectId);
|
|
213
|
+
if (project) return {
|
|
214
|
+
id,
|
|
215
|
+
label: project.name,
|
|
216
|
+
route: `/projects/${encodeURIComponent(objectId)}`
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (kind === "source-item") {
|
|
220
|
+
const source = getKnowledgeSourceItem(objectId);
|
|
221
|
+
if (source && !source.deletedAt && source.metadata.workspaceId === workspaceId) {
|
|
222
|
+
const excerpt = source.normalizedText?.slice(0, 600) ?? "";
|
|
223
|
+
return {
|
|
224
|
+
id,
|
|
225
|
+
label: typeof source.metadata.title === "string" ? source.metadata.title.slice(0, 120) : excerpt.split("\n")[0]?.slice(0, 120) || source.itemType,
|
|
226
|
+
excerpt
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const event = db.prepare("SELECT type, occurred_at FROM proactive_events WHERE event_id = ? AND workspace_id = ?").get(id, workspaceId);
|
|
231
|
+
return {
|
|
232
|
+
id,
|
|
233
|
+
label: event ? `${event.type} · ${event.occurred_at}` : kind || "Evidence"
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
//#endregion
|
|
237
|
+
export { cardChanges, getCard, listCards, performCardAction, requireCardScope };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { NotificationService } from '../../notifications/service.js';
|
|
2
|
+
import type { InboxItem } from './types.js';
|
|
3
|
+
/** The outbox is acknowledged only after a notification or suppression is persisted. */
|
|
4
|
+
export declare function deliverProactiveCard(item: InboxItem, notifications: NotificationService, publish: (type: string, payload: unknown) => void): void | {
|
|
5
|
+
retryAt: string;
|
|
6
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { getSqliteDatabase, runSqliteWriteTransaction } from "../../storage/sqlite/transaction.js";
|
|
2
|
+
import { effectiveProactivePolicy, localProactiveDay, nextDigestTime, quietHoursEnd, reserveProactiveNotification } from "../policy/service.js";
|
|
3
|
+
import { viewingProactiveCards } from "../policy/presence.js";
|
|
4
|
+
import { insightSourcesAuthorized } from "../execution/authorization.js";
|
|
5
|
+
import { insightCorrelation } from "./lifecycle.js";
|
|
6
|
+
import { getInboxItem } from "./repository.js";
|
|
7
|
+
import { queueDigest } from "./digest.js";
|
|
8
|
+
import { notificationPlanFromGatewayEvent } from "../../notifications/planner.js";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
//#region src/proactive/inbox/delivery.ts
|
|
11
|
+
/** The outbox is acknowledged only after a notification or suppression is persisted. */
|
|
12
|
+
function deliverProactiveCard(item, notifications, publish) {
|
|
13
|
+
const result = runSqliteWriteTransaction(() => {
|
|
14
|
+
const current = getInboxItem(item.id);
|
|
15
|
+
if (!current || current.withdrawnAt || !insightSourcesAuthorized(current.insightId) || current.status === "resolved" || current.expiresAt && Date.parse(current.expiresAt) <= Date.now()) return null;
|
|
16
|
+
if (current.status === "snoozed") return { retryAt: current.snoozedUntil };
|
|
17
|
+
const policy = effectiveProactivePolicy(current.subscriptionId);
|
|
18
|
+
if (!policy.enabled || policy.settings.delivery === "inbox") return null;
|
|
19
|
+
if ((policy.preferences.digestEnabled || policy.settings.delivery === "digest") && current.insight.attentionKind !== "receipt") {
|
|
20
|
+
queueDigest(current, "daily", nextDigestTime(policy.preferences, /* @__PURE__ */ new Date()));
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
if (!notificationPlanFromGatewayEvent("proactive.inbox.created", current)) return null;
|
|
24
|
+
if (policy.level === "quiet") return null;
|
|
25
|
+
const quietUntil = quietHoursEnd(policy.preferences, /* @__PURE__ */ new Date());
|
|
26
|
+
if (quietUntil) {
|
|
27
|
+
queueDigest(current, "quiet", quietUntil);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const db = getSqliteDatabase();
|
|
31
|
+
const key = `proactive:${insightCorrelation(current.insightId)}:${createHash("sha256").update(JSON.stringify([current.insight.urgency, current.insight.decision ?? null])).digest("hex")}:${localProactiveDay(/* @__PURE__ */ new Date(), policy.preferences.timezone)}`;
|
|
32
|
+
if (db.prepare("SELECT 1 FROM proactive_delivery_decisions WHERE notification_key = ?").get(key)) return null;
|
|
33
|
+
if (policy.preferences.suppressWhileViewing && viewingProactiveCards(policy.workspaceId)) {
|
|
34
|
+
db.prepare("INSERT INTO proactive_delivery_decisions VALUES (?, ?, ?, ?)").run(key, policy.workspaceId, "visible_in_app", (/* @__PURE__ */ new Date()).toISOString());
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const decision = reserveProactiveNotification(current.subscriptionId, key);
|
|
38
|
+
if (decision instanceof Date) return { retryAt: decision.toISOString() };
|
|
39
|
+
if (decision === "suppressed") return null;
|
|
40
|
+
const result = notifications.persistGatewayEvent("proactive.inbox.created", current);
|
|
41
|
+
db.prepare("INSERT INTO proactive_delivery_decisions VALUES (?, ?, ?, ?)").run(key, policy.workspaceId, "immediate", (/* @__PURE__ */ new Date()).toISOString());
|
|
42
|
+
return result;
|
|
43
|
+
});
|
|
44
|
+
publish("proactive.card.changed", {
|
|
45
|
+
cardId: item.id,
|
|
46
|
+
revision: item.revision
|
|
47
|
+
});
|
|
48
|
+
if (result && "retryAt" in result) return result;
|
|
49
|
+
if (result) publish("notification.created", result);
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { deliverProactiveCard };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ProductNotification } from '@xopcai/gateway-contract';
|
|
2
|
+
import type { NotificationPlan } from '../../notifications/planner.js';
|
|
3
|
+
import type { InboxItem } from './types.js';
|
|
4
|
+
export { nextDigestTime } from '../policy/service.js';
|
|
5
|
+
export declare function queueDigest(item: InboxItem, mode: 'daily' | 'quiet', dueAt: Date): void;
|
|
6
|
+
export declare function digestCards(id: string, workspaceId: string): import("@xopcai/gateway-contract").ProactiveCard[];
|
|
7
|
+
export declare function reserveWorkspaceAttention(workspaceId: string, key: string, now?: Date): boolean;
|
|
8
|
+
export declare function flushDueDigests(persist: (plan: NotificationPlan) => ProductNotification | null, now?: Date): ProductNotification[];
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { getSqliteDatabase, runSqliteWriteTransaction } from "../../storage/sqlite/transaction.js";
|
|
2
|
+
import { effectiveProactivePolicy, localProactiveDay, nextDigestTime, proactivePreferences, quietHoursEnd } from "../policy/service.js";
|
|
3
|
+
import { viewingProactiveCards } from "../policy/presence.js";
|
|
4
|
+
import { insightSourcesAuthorized } from "../execution/authorization.js";
|
|
5
|
+
import { getInboxItem } from "./repository.js";
|
|
6
|
+
import { getCard } from "./cards.js";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
//#region src/proactive/inbox/digest.ts
|
|
9
|
+
function queueDigest(item, mode, dueAt) {
|
|
10
|
+
const policy = effectiveProactivePolicy(item.subscriptionId);
|
|
11
|
+
getSqliteDatabase().prepare(`INSERT INTO proactive_digest_queue(inbox_item_id, workspace_id, notification_revision, mode, due_at)
|
|
12
|
+
VALUES (?, ?, ?, ?, ?) ON CONFLICT(inbox_item_id) DO UPDATE SET notification_revision = excluded.notification_revision,
|
|
13
|
+
mode = excluded.mode, due_at = excluded.due_at, consumed_at = NULL
|
|
14
|
+
WHERE proactive_digest_queue.notification_revision <> excluded.notification_revision`).run(item.id, policy.workspaceId, item.notificationRevision, mode, dueAt.toISOString());
|
|
15
|
+
}
|
|
16
|
+
function digestCards(id, workspaceId) {
|
|
17
|
+
const digest = getSqliteDatabase().prepare("SELECT * FROM proactive_digests WHERE digest_id = ? AND workspace_id = ?").get(id, workspaceId);
|
|
18
|
+
if (!digest) throw new Error("Digest not found");
|
|
19
|
+
return getSqliteDatabase().prepare("SELECT inbox_item_id, notification_revision FROM proactive_digest_members WHERE digest_id = ?").all(id).flatMap((member) => {
|
|
20
|
+
const item = getInboxItem(member.inbox_item_id);
|
|
21
|
+
if (!item || !eligibleDigestItem(item, digest.occurrence_key.startsWith("daily:") ? "daily" : "quiet") || item.notificationRevision !== member.notification_revision) return [];
|
|
22
|
+
const card = getCard(item.id, workspaceId);
|
|
23
|
+
return card.status === "withdrawn" ? [] : [card];
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function eligibleDigestItem(item, mode) {
|
|
27
|
+
const policy = effectiveProactivePolicy(item.subscriptionId);
|
|
28
|
+
const daily = policy.preferences.digestEnabled || policy.settings.delivery === "digest";
|
|
29
|
+
if (mode === "daily" ? !daily : policy.level === "quiet" && !daily) return false;
|
|
30
|
+
return Boolean(policy.enabled && policy.settings.delivery !== "inbox" && !item.withdrawnAt && !["resolved", "snoozed"].includes(item.status) && (!item.expiresAt || Date.parse(item.expiresAt) > Date.now()) && insightSourcesAuthorized(item.insightId));
|
|
31
|
+
}
|
|
32
|
+
function reserveWorkspaceAttention(workspaceId, key, now = /* @__PURE__ */ new Date()) {
|
|
33
|
+
const db = getSqliteDatabase();
|
|
34
|
+
const preferences = proactivePreferences(workspaceId);
|
|
35
|
+
if (preferences.level === "off" || preferences.pausedUntil && Date.parse(preferences.pausedUntil) > now.getTime()) return false;
|
|
36
|
+
if (db.prepare("SELECT 1 FROM proactive_notification_budget WHERE dedupe_key = ?").get(key)) return true;
|
|
37
|
+
const day = localProactiveDay(now, preferences.timezone);
|
|
38
|
+
const { count } = db.prepare("SELECT COUNT(*) AS count FROM proactive_notification_budget WHERE workspace_id = ? AND local_day = ?").get(workspaceId, day);
|
|
39
|
+
if (count >= preferences.dailyNotificationLimit) return false;
|
|
40
|
+
db.prepare("INSERT INTO proactive_notification_budget VALUES (?, ?, ?, ?)").run(key, workspaceId, day, now.toISOString());
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
function flushDueDigests(persist, now = /* @__PURE__ */ new Date()) {
|
|
44
|
+
return runSqliteWriteTransaction((db) => {
|
|
45
|
+
const groups = db.prepare(`SELECT workspace_id, mode FROM proactive_digest_queue WHERE consumed_at IS NULL AND due_at <= ? GROUP BY workspace_id, mode LIMIT 20`).all(now.toISOString());
|
|
46
|
+
const notifications = [];
|
|
47
|
+
for (const group of groups) {
|
|
48
|
+
const preferences = proactivePreferences(group.workspace_id);
|
|
49
|
+
const until = quietHoursEnd(preferences, now);
|
|
50
|
+
if (until) {
|
|
51
|
+
db.prepare("UPDATE proactive_digest_queue SET due_at = ? WHERE workspace_id = ? AND mode = ? AND consumed_at IS NULL").run(until.toISOString(), group.workspace_id, group.mode);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const queued = db.prepare("SELECT inbox_item_id FROM proactive_digest_queue WHERE workspace_id = ? AND mode = ? AND consumed_at IS NULL AND due_at <= ? ORDER BY due_at LIMIT 500").all(group.workspace_id, group.mode, now.toISOString());
|
|
55
|
+
const seen = /* @__PURE__ */ new Set();
|
|
56
|
+
const items = queued.flatMap((row) => {
|
|
57
|
+
const item = getInboxItem(row.inbox_item_id);
|
|
58
|
+
if (!item || !eligibleDigestItem(item, group.mode)) return [];
|
|
59
|
+
const key = item.correlationKey ?? item.id;
|
|
60
|
+
if (seen.has(key)) return [];
|
|
61
|
+
seen.add(key);
|
|
62
|
+
return [item];
|
|
63
|
+
});
|
|
64
|
+
const occurrence = `${group.mode}:${localProactiveDay(now, preferences.timezone)}`;
|
|
65
|
+
if (db.prepare("SELECT 1 FROM proactive_digests WHERE workspace_id = ? AND occurrence_key = ?").get(group.workspace_id, occurrence) && items.length) {
|
|
66
|
+
const tomorrow = nextDigestTime(preferences, new Date(now.getTime() + 6e4));
|
|
67
|
+
db.prepare("UPDATE proactive_digest_queue SET due_at = ? WHERE workspace_id = ? AND mode = ? AND consumed_at IS NULL").run(tomorrow.toISOString(), group.workspace_id, group.mode);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (!items.length || preferences.suppressWhileViewing && viewingProactiveCards(group.workspace_id, now.getTime())) {
|
|
71
|
+
for (const row of queued) db.prepare("UPDATE proactive_digest_queue SET consumed_at = ? WHERE inbox_item_id = ?").run(now.toISOString(), row.inbox_item_id);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const key = `proactive.digest:${group.workspace_id}:${occurrence}`;
|
|
75
|
+
if (!reserveWorkspaceAttention(group.workspace_id, key, now)) {
|
|
76
|
+
db.prepare("UPDATE proactive_digest_queue SET due_at = ? WHERE workspace_id = ? AND mode = ? AND consumed_at IS NULL").run(nextDigestTime(preferences, new Date(now.getTime() + 6e4)).toISOString(), group.workspace_id, group.mode);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const id = randomUUID();
|
|
80
|
+
db.prepare("INSERT INTO proactive_digests(digest_id, workspace_id, occurrence_key, created_at) VALUES (?, ?, ?, ?)").run(id, group.workspace_id, occurrence, now.toISOString());
|
|
81
|
+
for (const item of items) db.prepare("INSERT INTO proactive_digest_members VALUES (?, ?, ?)").run(id, item.id, item.notificationRevision);
|
|
82
|
+
const notification = persist({
|
|
83
|
+
dedupeKey: key,
|
|
84
|
+
notification: {
|
|
85
|
+
type: "proactive.insight",
|
|
86
|
+
target: {
|
|
87
|
+
kind: "proactive_digest",
|
|
88
|
+
digestId: id
|
|
89
|
+
},
|
|
90
|
+
priority: "normal",
|
|
91
|
+
title: {
|
|
92
|
+
en: "Your work digest is ready",
|
|
93
|
+
zh: "你的工作摘要已就绪"
|
|
94
|
+
},
|
|
95
|
+
body: {
|
|
96
|
+
en: `${items.length} updates to review in xopc.`,
|
|
97
|
+
zh: `有 ${items.length} 项变化,打开 xopc 查看。`
|
|
98
|
+
},
|
|
99
|
+
payload: { digestId: id }
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
if (notification) {
|
|
103
|
+
db.prepare("UPDATE proactive_digests SET notification_id = ? WHERE digest_id = ?").run(notification.id, id);
|
|
104
|
+
notifications.push(notification);
|
|
105
|
+
}
|
|
106
|
+
for (const row of queued) db.prepare("UPDATE proactive_digest_queue SET consumed_at = ? WHERE inbox_item_id = ?").run(now.toISOString(), row.inbox_item_id);
|
|
107
|
+
}
|
|
108
|
+
return notifications;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
export { digestCards, flushDueDigests, nextDigestTime, queueDigest, reserveWorkspaceAttention };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function withdrawCard(id: string, now?: Date): void;
|
|
2
|
+
/** Conservative correlation: the same scope, evidence subjects and requested outcome. */
|
|
3
|
+
export declare function insightCorrelation(insightId: string): string;
|
|
4
|
+
export declare function reconcileCards(now?: Date): number;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { getSqliteDatabase, runSqliteWriteTransaction } from "../../storage/sqlite/transaction.js";
|
|
2
|
+
import { insightSourcesAuthorized } from "../execution/authorization.js";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
//#region src/proactive/inbox/lifecycle.ts
|
|
5
|
+
function withdrawCard(id, now = /* @__PURE__ */ new Date()) {
|
|
6
|
+
runSqliteWriteTransaction((db) => {
|
|
7
|
+
db.prepare("UPDATE proactive_inbox_items SET withdrawn_at = ?, updated_at = ? WHERE inbox_item_id = ? AND withdrawn_at IS NULL").run(now.toISOString(), now.toISOString(), id);
|
|
8
|
+
db.prepare("UPDATE proactive_delivery_outbox SET status = 'delivered', error_message = 'source_withdrawn' WHERE inbox_item_id = ? AND status <> 'delivered'").run(id);
|
|
9
|
+
db.prepare("UPDATE proactive_insights SET action_status = 'rejected', action_error = 'source_withdrawn' WHERE insight_id = (SELECT insight_id FROM proactive_inbox_items WHERE inbox_item_id = ?) AND action_status IN ('pending', 'approval_required', 'failed')").run(id);
|
|
10
|
+
db.prepare(`UPDATE notification_events SET title_en = 'Update withdrawn', title_zh = '消息已撤回', body_en = NULL, body_zh = NULL
|
|
11
|
+
WHERE event_type = 'proactive.insight' AND json_extract(target_json, '$.inboxItemId') = ?`).run(id);
|
|
12
|
+
db.prepare(`UPDATE proactive_web_push_deliveries SET status = 'failed', lease_until = NULL, last_error = 'source_withdrawn'
|
|
13
|
+
WHERE inbox_item_id = ? AND status IN ('pending', 'sending')`).run(id);
|
|
14
|
+
db.prepare(`UPDATE notification_deliveries SET status = 'dead', updated_at = ? WHERE status IN ('pending', 'accepted') AND event_id IN
|
|
15
|
+
(SELECT event_id FROM notification_events WHERE event_type = 'proactive.insight' AND json_extract(target_json, '$.inboxItemId') = ?)`).run(now.getTime(), id);
|
|
16
|
+
db.prepare(`UPDATE proactive_channel_deliveries SET status = 'cancelled', lease_until = NULL, last_error = 'source_withdrawn' WHERE status IN ('pending', 'sending') AND notification_id IN
|
|
17
|
+
(SELECT event_id FROM notification_events WHERE event_type = 'proactive.insight' AND json_extract(target_json, '$.inboxItemId') = ?)`).run(id);
|
|
18
|
+
db.prepare("DELETE FROM proactive_digest_queue WHERE inbox_item_id = ?").run(id);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/** Conservative correlation: the same scope, evidence subjects and requested outcome. */
|
|
22
|
+
function insightCorrelation(insightId) {
|
|
23
|
+
const row = getSqliteDatabase().prepare(`SELECT s.workspace_id, b.aggregation_key, x.evidence_ids_json, x.proposed_action_json, x.recommendation
|
|
24
|
+
FROM proactive_insights x JOIN proactive_runs r USING(run_id) JOIN proactive_signal_batches b USING(batch_id)
|
|
25
|
+
JOIN proactive_scenario_subscriptions s ON s.subscription_id = x.subscription_id WHERE x.insight_id = ?`).get(insightId);
|
|
26
|
+
if (!row) return insightId;
|
|
27
|
+
const normalize = (value) => value.normalize("NFKC").trim().toLowerCase().replace(/\s+/g, " ");
|
|
28
|
+
const evidence = JSON.parse(row.evidence_ids_json).filter((id) => /^(task|project|source-item|automation-run):/.test(id)).sort();
|
|
29
|
+
if (!evidence.length) return insightId;
|
|
30
|
+
const action = row.proposed_action_json ? JSON.parse(row.proposed_action_json) : null;
|
|
31
|
+
return createHash("sha256").update(JSON.stringify([
|
|
32
|
+
row.workspace_id,
|
|
33
|
+
row.aggregation_key,
|
|
34
|
+
evidence,
|
|
35
|
+
normalize(action ? JSON.stringify([action.id, action.input]) : row.recommendation)
|
|
36
|
+
])).digest("hex");
|
|
37
|
+
}
|
|
38
|
+
function reconcileCards(now = /* @__PURE__ */ new Date()) {
|
|
39
|
+
return runSqliteWriteTransaction((db) => {
|
|
40
|
+
const rows = db.prepare(`SELECT i.inbox_item_id, i.insight_id, i.correlation_key FROM proactive_inbox_items i
|
|
41
|
+
LEFT JOIN proactive_card_review_state r USING(inbox_item_id) WHERE i.withdrawn_at IS NULL
|
|
42
|
+
ORDER BY COALESCE(r.checked_at, ''), i.created_at LIMIT 200`).all();
|
|
43
|
+
let withdrawn = 0;
|
|
44
|
+
for (const row of rows) {
|
|
45
|
+
if (!insightSourcesAuthorized(row.insight_id)) {
|
|
46
|
+
withdrawCard(row.inbox_item_id, now);
|
|
47
|
+
withdrawn++;
|
|
48
|
+
} else {
|
|
49
|
+
const correlation = insightCorrelation(row.insight_id);
|
|
50
|
+
if (row.correlation_key !== correlation) db.prepare("UPDATE proactive_inbox_items SET correlation_key = ? WHERE inbox_item_id = ?").run(correlation, row.inbox_item_id);
|
|
51
|
+
}
|
|
52
|
+
db.prepare(`INSERT INTO proactive_card_review_state VALUES (?, ?) ON CONFLICT(inbox_item_id) DO UPDATE SET checked_at = excluded.checked_at`).run(row.inbox_item_id, now.toISOString());
|
|
53
|
+
}
|
|
54
|
+
return withdrawn;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
export { insightCorrelation, reconcileCards, withdrawCard };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function recheckProactiveNotification(itemId: string, now?: Date, notificationRevision?: unknown): 'send' | 'cancel' | Date;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { effectiveProactivePolicy, quietHoursEnd } from "../policy/service.js";
|
|
2
|
+
import { insightSourcesAuthorized } from "../execution/authorization.js";
|
|
3
|
+
import { getInboxItem } from "./repository.js";
|
|
4
|
+
//#region src/proactive/inbox/notification-policy.ts
|
|
5
|
+
function recheckProactiveNotification(itemId, now = /* @__PURE__ */ new Date(), notificationRevision) {
|
|
6
|
+
const item = getInboxItem(itemId);
|
|
7
|
+
if (!item || item.withdrawnAt || !insightSourcesAuthorized(item.insightId) || item.status === "resolved" || item.expiresAt && Date.parse(item.expiresAt) <= now.getTime()) return "cancel";
|
|
8
|
+
if (typeof notificationRevision === "number" && notificationRevision !== item.notificationRevision) return "cancel";
|
|
9
|
+
const policy = effectiveProactivePolicy(item.subscriptionId, now);
|
|
10
|
+
if (!policy.enabled || policy.preferences.digestEnabled && item.insight.attentionKind !== "receipt") return "cancel";
|
|
11
|
+
if (item.status === "snoozed") return new Date(item.snoozedUntil);
|
|
12
|
+
if (policy.settings.managed || policy.preferences.revision > 0) {
|
|
13
|
+
if (policy.level === "quiet" || policy.settings.delivery !== "important") return "cancel";
|
|
14
|
+
const until = quietHoursEnd(policy.preferences, now);
|
|
15
|
+
if (until) return until;
|
|
16
|
+
}
|
|
17
|
+
return "send";
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { recheckProactiveNotification };
|
|
@@ -22,3 +22,4 @@ export interface OutboxClaim {
|
|
|
22
22
|
export declare function claimDelivery(now?: Date): OutboxClaim | null;
|
|
23
23
|
export declare function finishDelivery(id: string, error?: unknown, attempt?: number, now?: Date): void;
|
|
24
24
|
export declare function recoverExpiredDeliveries(now?: Date): number;
|
|
25
|
+
export declare function deferDelivery(id: string, attempt: number, retryAt: string): void;
|