botschat 0.1.13 → 0.1.14

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.
Files changed (39) hide show
  1. package/package.json +1 -1
  2. package/packages/api/src/do/connection-do.ts +103 -19
  3. package/packages/api/src/env.ts +6 -0
  4. package/packages/api/src/index.ts +25 -2
  5. package/packages/api/src/utils/apns.ts +151 -0
  6. package/packages/api/src/utils/fcm.ts +23 -32
  7. package/packages/plugin/dist/src/channel.d.ts.map +1 -1
  8. package/packages/plugin/dist/src/channel.js +25 -2
  9. package/packages/plugin/dist/src/channel.js.map +1 -1
  10. package/packages/plugin/dist/src/types.d.ts +5 -0
  11. package/packages/plugin/dist/src/types.d.ts.map +1 -1
  12. package/packages/plugin/dist/src/ws-client.d.ts +1 -0
  13. package/packages/plugin/dist/src/ws-client.d.ts.map +1 -1
  14. package/packages/plugin/dist/src/ws-client.js +1 -0
  15. package/packages/plugin/dist/src/ws-client.js.map +1 -1
  16. package/packages/plugin/package.json +1 -1
  17. package/packages/web/dist/assets/index-BJye3VHV.js +1516 -0
  18. package/packages/web/dist/assets/{index-Bd_RDcgO.css → index-CNSCbd7_.css} +1 -1
  19. package/packages/web/dist/assets/index-CPOiRHa4.js +2 -0
  20. package/packages/web/dist/assets/{index-lVB82JKU.js → index-CQPXprFz.js} +1 -1
  21. package/packages/web/dist/assets/{index-Civeg2lm.js → index-CkIgZfHf.js} +1 -1
  22. package/packages/web/dist/assets/index-DbUyNI4d.js +1 -0
  23. package/packages/web/dist/assets/index-Dpvhc_dU.js +2 -0
  24. package/packages/web/dist/assets/{index.esm-CtMkqqqb.js → index.esm-DgcFARs7.js} +1 -1
  25. package/packages/web/dist/assets/{web-vKLTVUul.js → web-Bfku9Io_.js} +1 -1
  26. package/packages/web/dist/assets/{web-CUXjh_UA.js → web-CnOlwlZw.js} +1 -1
  27. package/packages/web/dist/index.html +2 -2
  28. package/packages/web/src/App.tsx +92 -5
  29. package/packages/web/src/api.ts +2 -2
  30. package/packages/web/src/components/ChatWindow.tsx +20 -2
  31. package/packages/web/src/components/E2ESettings.tsx +70 -1
  32. package/packages/web/src/components/MobileLayout.tsx +7 -0
  33. package/packages/web/src/foreground.ts +5 -1
  34. package/packages/web/src/push.ts +27 -3
  35. package/scripts/mock-openclaw.mjs +13 -3
  36. package/packages/web/dist/assets/index-B9qN5gs6.js +0 -1
  37. package/packages/web/dist/assets/index-BQNMGVyU.js +0 -2
  38. package/packages/web/dist/assets/index-Dk33VSnY.js +0 -2
  39. package/packages/web/dist/assets/index-Kr85Nj_-.js +0 -1516
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botschat",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "A self-hosted chat interface for OpenClaw AI agents",
5
5
  "workspaces": [
6
6
  "packages/*"
@@ -1,6 +1,7 @@
1
1
  import type { Env } from "../env.js";
2
2
  import { verifyToken, getJwtSecret, signMediaUrl } from "../utils/auth.js";
3
3
  import { getFcmAccessToken, sendPushNotification } from "../utils/fcm.js";
4
+ import { sendApnsNotification, type ApnsConfig } from "../utils/apns.js";
4
5
  import { generateId as generateIdUtil } from "../utils/id.js";
5
6
  import { randomUUID } from "../utils/uuid.js";
6
7
 
@@ -217,6 +218,9 @@ export class ConnectionDO implements DurableObject {
217
218
  // After auth, request task scan + models list from the plugin
218
219
  ws.send(JSON.stringify({ type: "task.scan.request" }));
219
220
  ws.send(JSON.stringify({ type: "models.request" }));
221
+ // Send notification preview preference to plugin
222
+ const notifyPreview = await this.getNotifyPreviewSetting(userId);
223
+ ws.send(JSON.stringify({ type: "settings.notifyPreview", enabled: notifyPreview }));
220
224
  // Notify all browser clients that OpenClaw is now connected
221
225
  this.broadcastToBrowsers(
222
226
  JSON.stringify({ type: "connection.status", openclawConnected: true, defaultModel: this.defaultModel, models: this.cachedModels }),
@@ -315,17 +319,19 @@ export class ConnectionDO implements DurableObject {
315
319
  await this.handleJobUpdate(msg);
316
320
  }
317
321
 
318
- // Forward all messages to browser clients
322
+ // Forward all messages to browser clients (strip notifyPreview — plaintext
323
+ // must not be relayed to browser WebSockets; browsers decrypt locally)
319
324
  if (msg.type === "agent.text") {
320
325
  console.log(`[DO] Forwarding agent.text to browsers: encrypted=${msg.encrypted}, messageId=${msg.messageId}, textLen=${typeof msg.text === "string" ? msg.text.length : "?"}`);
321
326
  }
322
- this.broadcastToBrowsers(JSON.stringify(msg));
327
+ const { notifyPreview: _stripped, ...msgForBrowser } = msg;
328
+ this.broadcastToBrowsers(JSON.stringify(msgForBrowser));
323
329
 
324
330
  // Send push notification if no browser session is in foreground
325
331
  if (
326
332
  (msg.type === "agent.text" || msg.type === "agent.media" || msg.type === "agent.a2ui") &&
327
333
  this.foregroundSessions.size === 0 &&
328
- this.env.FCM_SERVICE_ACCOUNT_JSON
334
+ (this.env.FCM_SERVICE_ACCOUNT_JSON || this.env.APNS_AUTH_KEY)
329
335
  ) {
330
336
  this.sendPushNotifications(msg).catch((err) => {
331
337
  console.error("[DO] Push notification failed:", err);
@@ -546,6 +552,24 @@ export class ConnectionDO implements DurableObject {
546
552
 
547
553
  // ---- Helpers ----
548
554
 
555
+ /** Read the user's notifyPreview preference from D1 settings_json. */
556
+ private async getNotifyPreviewSetting(userId?: string | null): Promise<boolean> {
557
+ const uid = userId ?? await this.state.storage.get<string>("userId");
558
+ if (!uid) return false;
559
+ try {
560
+ const row = await this.env.DB.prepare(
561
+ "SELECT settings_json FROM users WHERE id = ?",
562
+ ).bind(uid).first<{ settings_json: string }>();
563
+ if (row?.settings_json) {
564
+ const settings = JSON.parse(row.settings_json);
565
+ return settings.notifyPreview === true;
566
+ }
567
+ } catch (err) {
568
+ console.error("[DO] Failed to read notifyPreview setting:", err);
569
+ }
570
+ return false;
571
+ }
572
+
549
573
  /** Restore cachedModels and defaultModel from durable storage if in-memory cache is empty. */
550
574
  private async ensureCachedModels(): Promise<void> {
551
575
  if (this.cachedModels.length > 0) return;
@@ -599,7 +623,9 @@ export class ConnectionDO implements DurableObject {
599
623
  /**
600
624
  * Send push notifications to all of the user's registered devices.
601
625
  * Called when an agent message arrives and no browser session is in foreground.
602
- * Sends data-only FCM messages so clients can decrypt E2E content before showing.
626
+ *
627
+ * iOS tokens go directly to APNs (Capacitor returns raw APNs device tokens).
628
+ * Web/Android tokens go through FCM HTTP v1 API.
603
629
  */
604
630
  private async sendPushNotifications(msg: Record<string, unknown>): Promise<void> {
605
631
  const userId = await this.state.storage.get<string>("userId");
@@ -612,8 +638,8 @@ export class ConnectionDO implements DurableObject {
612
638
  .all<{ id: string; token: string; platform: string }>();
613
639
 
614
640
  if (!results || results.length === 0) return;
641
+ console.log(`[DO] Push: ${results.length} token(s) for ${userId} (ios: ${results.filter((r) => r.platform === "ios").length})`);
615
642
 
616
- // Build data payload — includes ciphertext so the client can decrypt
617
643
  const data: Record<string, string> = {
618
644
  type: msg.type as string,
619
645
  sessionKey: (msg.sessionKey as string) ?? "",
@@ -621,30 +647,88 @@ export class ConnectionDO implements DurableObject {
621
647
  encrypted: msg.encrypted ? "1" : "0",
622
648
  };
623
649
 
650
+ let notifBody = "New message";
624
651
  if (msg.type === "agent.text") {
625
652
  data.text = (msg.text as string) ?? "";
653
+ if (msg.encrypted && typeof msg.notifyPreview === "string" && msg.notifyPreview) {
654
+ const preview = msg.notifyPreview as string;
655
+ notifBody = preview.length > 100 ? preview.slice(0, 100) + "…" : preview;
656
+ } else if (!msg.encrypted && data.text) {
657
+ notifBody = data.text.length > 100 ? data.text.slice(0, 100) + "…" : data.text;
658
+ } else if (msg.encrypted) {
659
+ notifBody = "New encrypted message";
660
+ }
626
661
  } else if (msg.type === "agent.media") {
627
662
  data.text = (msg.caption as string) || "";
628
663
  data.mediaUrl = (msg.mediaUrl as string) ?? "";
664
+ notifBody = data.text || "Sent a media file";
629
665
  } else if (msg.type === "agent.a2ui") {
630
666
  data.text = "New interactive message";
667
+ notifBody = "New interactive message";
631
668
  }
632
669
 
633
- const accessToken = await getFcmAccessToken(this.env.FCM_SERVICE_ACCOUNT_JSON!);
634
- const projectId = this.env.FIREBASE_PROJECT_ID ?? "botschat-130ff";
635
-
670
+ const iosTokens = results.filter((r) => r.platform === "ios");
671
+ const androidTokens = results.filter((r) => r.platform === "android");
672
+ const webTokens = results.filter((r) => r.platform === "web");
636
673
  const invalidTokenIds: string[] = [];
637
- await Promise.allSettled(
638
- results.map(async (row) => {
639
- const ok = await sendPushNotification({
640
- accessToken,
641
- projectId,
642
- fcmToken: row.token,
643
- data,
644
- });
645
- if (!ok) invalidTokenIds.push(row.id);
646
- }),
647
- );
674
+ const notification = { title: "BotsChat", body: notifBody };
675
+
676
+ // iOS: send via APNs directly (Capacitor registers raw APNs device tokens)
677
+ if (iosTokens.length > 0 && this.env.APNS_AUTH_KEY) {
678
+ const apnsConfig: ApnsConfig = {
679
+ authKey: this.env.APNS_AUTH_KEY,
680
+ keyId: this.env.APNS_KEY_ID ?? "",
681
+ teamId: this.env.APNS_TEAM_ID ?? "",
682
+ bundleId: "app.botschat.console",
683
+ };
684
+ await Promise.allSettled(
685
+ iosTokens.map(async (row) => {
686
+ const ok = await sendApnsNotification({
687
+ config: apnsConfig,
688
+ deviceToken: row.token,
689
+ title: "BotsChat",
690
+ body: notifBody,
691
+ data,
692
+ });
693
+ if (!ok) invalidTokenIds.push(row.id);
694
+ }),
695
+ );
696
+ }
697
+
698
+ // Android: FCM with notification payload (data-only is silent in background)
699
+ if (androidTokens.length > 0 && this.env.FCM_SERVICE_ACCOUNT_JSON) {
700
+ const accessToken = await getFcmAccessToken(this.env.FCM_SERVICE_ACCOUNT_JSON);
701
+ const projectId = this.env.FIREBASE_PROJECT_ID ?? "botschat-130ff";
702
+ await Promise.allSettled(
703
+ androidTokens.map(async (row) => {
704
+ const ok = await sendPushNotification({
705
+ accessToken,
706
+ projectId,
707
+ fcmToken: row.token,
708
+ data,
709
+ notification,
710
+ });
711
+ if (!ok) invalidTokenIds.push(row.id);
712
+ }),
713
+ );
714
+ }
715
+
716
+ // Web: FCM data-only (Service Worker decrypts + shows notification)
717
+ if (webTokens.length > 0 && this.env.FCM_SERVICE_ACCOUNT_JSON) {
718
+ const accessToken = await getFcmAccessToken(this.env.FCM_SERVICE_ACCOUNT_JSON);
719
+ const projectId = this.env.FIREBASE_PROJECT_ID ?? "botschat-130ff";
720
+ await Promise.allSettled(
721
+ webTokens.map(async (row) => {
722
+ const ok = await sendPushNotification({
723
+ accessToken,
724
+ projectId,
725
+ fcmToken: row.token,
726
+ data,
727
+ });
728
+ if (!ok) invalidTokenIds.push(row.id);
729
+ }),
730
+ );
731
+ }
648
732
 
649
733
  // Clean up invalid/expired tokens
650
734
  for (const id of invalidTokenIds) {
@@ -14,4 +14,10 @@ export type Env = {
14
14
  DEV_AUTH_SECRET?: string;
15
15
  /** FCM Service Account JSON for push notifications (stored as secret via `wrangler secret put`). */
16
16
  FCM_SERVICE_ACCOUNT_JSON?: string;
17
+ /** APNs Auth Key (.p8 content) for direct iOS push via APNs HTTP/2 API. */
18
+ APNS_AUTH_KEY?: string;
19
+ /** APNs Key ID (from Apple Developer portal, e.g. "3Q4V693LW4"). */
20
+ APNS_KEY_ID?: string;
21
+ /** Apple Developer Team ID (e.g. "C5N5PPC329"). */
22
+ APNS_TEAM_ID?: string;
17
23
  };
@@ -124,7 +124,7 @@ protectedApp.get("/me", async (c) => {
124
124
 
125
125
  protectedApp.patch("/me", async (c) => {
126
126
  const userId = c.get("userId");
127
- const body = await c.req.json<{ defaultModel?: string }>();
127
+ const body = await c.req.json<{ defaultModel?: string; notifyPreview?: boolean }>();
128
128
 
129
129
  // defaultModel is not stored in D1 — get/set only via plugin (connection.status / push).
130
130
  const existing = await c.env.DB.prepare(
@@ -135,7 +135,11 @@ protectedApp.patch("/me", async (c) => {
135
135
 
136
136
  const settings = JSON.parse(existing?.settings_json || "{}");
137
137
  delete settings.defaultModel;
138
- // Persist other settings (if any) to D1; defaultModel is never written.
138
+
139
+ if (body.notifyPreview !== undefined) {
140
+ settings.notifyPreview = body.notifyPreview;
141
+ }
142
+
139
143
  await c.env.DB.prepare(
140
144
  "UPDATE users SET settings_json = ? WHERE id = ?",
141
145
  )
@@ -161,6 +165,25 @@ protectedApp.patch("/me", async (c) => {
161
165
  }
162
166
  }
163
167
 
168
+ if (body.notifyPreview !== undefined) {
169
+ try {
170
+ const doId = c.env.CONNECTION_DO.idFromName(userId);
171
+ const stub = c.env.CONNECTION_DO.get(doId);
172
+ await stub.fetch(
173
+ new Request("https://internal/send", {
174
+ method: "POST",
175
+ headers: { "Content-Type": "application/json" },
176
+ body: JSON.stringify({
177
+ type: "settings.notifyPreview",
178
+ enabled: body.notifyPreview,
179
+ }),
180
+ }),
181
+ );
182
+ } catch (err) {
183
+ console.error("Failed to push notifyPreview to OpenClaw:", err);
184
+ }
185
+ }
186
+
164
187
  const outSettings = { ...settings };
165
188
  delete outSettings.defaultModel;
166
189
  return c.json({ ok: true, settings: outSettings });
@@ -0,0 +1,151 @@
1
+ /**
2
+ * APNs HTTP/2 API — send push notifications directly to iOS devices.
3
+ *
4
+ * Uses a .p8 Auth Key for token-based authentication (ES256 JWT).
5
+ * iOS devices register raw APNs device tokens (hex strings) via Capacitor,
6
+ * which cannot be sent through FCM. This module talks to APNs directly.
7
+ */
8
+
9
+ let cachedJwt: string | null = null;
10
+ let cachedJwtExpiry = 0;
11
+
12
+ export type ApnsConfig = {
13
+ authKey: string;
14
+ keyId: string;
15
+ teamId: string;
16
+ bundleId: string;
17
+ };
18
+
19
+ async function getApnsJwt(config: ApnsConfig): Promise<string> {
20
+ const now = Math.floor(Date.now() / 1000);
21
+ if (cachedJwt && cachedJwtExpiry > now + 300) {
22
+ return cachedJwt;
23
+ }
24
+
25
+ const header = { alg: "ES256", kid: config.keyId };
26
+ const claims = { iss: config.teamId, iat: now };
27
+
28
+ const key = await importP8Key(config.authKey);
29
+ cachedJwt = await signEs256Jwt(header, claims, key);
30
+ cachedJwtExpiry = now + 3600;
31
+ return cachedJwt;
32
+ }
33
+
34
+ export type ApnsPushOpts = {
35
+ config: ApnsConfig;
36
+ deviceToken: string;
37
+ title: string;
38
+ body: string;
39
+ /** Custom data included alongside the aps payload. */
40
+ data?: Record<string, string>;
41
+ };
42
+
43
+ /**
44
+ * Send a visible push notification via APNs.
45
+ * Returns true on success.
46
+ * Returns false if the token is permanently invalid (410 Unregistered)
47
+ * so the caller can clean it up.
48
+ */
49
+ export async function sendApnsNotification(opts: ApnsPushOpts): Promise<boolean> {
50
+ const jwt = await getApnsJwt(opts.config);
51
+
52
+ const payload: Record<string, unknown> = {
53
+ aps: {
54
+ alert: { title: opts.title, body: opts.body },
55
+ sound: "default",
56
+ "mutable-content": 1,
57
+ },
58
+ };
59
+ if (opts.data) {
60
+ payload.custom = opts.data;
61
+ }
62
+
63
+ const headers = {
64
+ Authorization: `Bearer ${jwt}`,
65
+ "apns-topic": opts.config.bundleId,
66
+ "apns-push-type": "alert",
67
+ "apns-priority": "10",
68
+ "apns-expiration": "0",
69
+ };
70
+ const body = JSON.stringify(payload);
71
+
72
+ // Try production first, fall back to sandbox for debug/TestFlight builds
73
+ for (const host of ["api.push.apple.com", "api.sandbox.push.apple.com"]) {
74
+ const res = await fetch(`https://${host}/3/device/${opts.deviceToken}`, {
75
+ method: "POST",
76
+ headers,
77
+ body,
78
+ });
79
+
80
+ if (res.ok) {
81
+ console.log(`[APNs] Sent via ${host} for ...${opts.deviceToken.slice(-8)}`);
82
+ return true;
83
+ }
84
+
85
+ const err = await res.text();
86
+ const isBadToken = err.includes("BadDeviceToken");
87
+
88
+ // BadDeviceToken on production → likely a sandbox token, try sandbox next
89
+ if (host === "api.push.apple.com" && res.status === 400 && isBadToken) {
90
+ console.log(`[APNs] Production rejected token, trying sandbox...`);
91
+ continue;
92
+ }
93
+
94
+ console.error(`[APNs] ${host} failed for ...${opts.deviceToken.slice(-8)}: ${res.status} ${err}`);
95
+
96
+ // 410 = device unregistered — safe to remove
97
+ if (res.status === 410) return false;
98
+ return !isBadToken;
99
+ }
100
+
101
+ return true;
102
+ }
103
+
104
+ // ---- ES256 JWT helpers ----
105
+
106
+ async function importP8Key(pem: string): Promise<CryptoKey> {
107
+ // Handle literal "\n" escape sequences from env vars (.dev.vars / wrangler secret)
108
+ const normalized = pem.replace(/\\n/g, "\n");
109
+ const pemBody = normalized
110
+ .replace(/-----BEGIN PRIVATE KEY-----/, "")
111
+ .replace(/-----END PRIVATE KEY-----/, "")
112
+ .replace(/\s/g, "");
113
+ const binary = atob(pemBody);
114
+ const der = new Uint8Array(binary.length);
115
+ for (let i = 0; i < binary.length; i++) {
116
+ der[i] = binary.charCodeAt(i);
117
+ }
118
+ return crypto.subtle.importKey(
119
+ "pkcs8",
120
+ der.buffer as ArrayBuffer,
121
+ { name: "ECDSA", namedCurve: "P-256" },
122
+ false,
123
+ ["sign"],
124
+ );
125
+ }
126
+
127
+ function base64url(data: Uint8Array | string): string {
128
+ const str =
129
+ typeof data === "string"
130
+ ? btoa(data)
131
+ : btoa(String.fromCharCode(...data));
132
+ return str.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
133
+ }
134
+
135
+ async function signEs256Jwt(
136
+ header: Record<string, string>,
137
+ payload: Record<string, unknown>,
138
+ key: CryptoKey,
139
+ ): Promise<string> {
140
+ const headerB64 = base64url(JSON.stringify(header));
141
+ const payloadB64 = base64url(JSON.stringify(payload));
142
+ const input = `${headerB64}.${payloadB64}`;
143
+
144
+ const sig = await crypto.subtle.sign(
145
+ { name: "ECDSA", hash: "SHA-256" },
146
+ key,
147
+ new TextEncoder().encode(input),
148
+ );
149
+
150
+ return `${input}.${base64url(new Uint8Array(sig))}`;
151
+ }
@@ -65,6 +65,8 @@ export type PushPayload = {
65
65
  fcmToken: string;
66
66
  /** Data payload — sent as FCM data-only message so client can decrypt + show notification. */
67
67
  data: Record<string, string>;
68
+ /** If set, include a visible notification (used for Android where data-only is silent in background). */
69
+ notification?: { title: string; body: string };
68
70
  };
69
71
 
70
72
  /**
@@ -75,35 +77,22 @@ export type PushPayload = {
75
77
  export async function sendPushNotification(opts: PushPayload): Promise<boolean> {
76
78
  const url = `https://fcm.googleapis.com/v1/projects/${opts.projectId}/messages:send`;
77
79
 
78
- const message = {
79
- message: {
80
- token: opts.fcmToken,
81
- // Data-only message — no "notification" field.
82
- // Client receives the data and shows a local notification after decryption.
83
- data: opts.data,
84
- // Platform-specific overrides for data-only delivery
85
- android: {
86
- priority: "high" as const,
87
- },
88
- apns: {
89
- headers: {
90
- "apns-priority": "10",
91
- "apns-push-type": "background",
92
- },
93
- payload: {
94
- aps: {
95
- "content-available": 1,
96
- sound: "default",
97
- },
98
- },
99
- },
100
- webpush: {
101
- headers: {
102
- Urgency: "high",
103
- },
80
+ const msg: Record<string, unknown> = {
81
+ token: opts.fcmToken,
82
+ data: opts.data,
83
+ android: {
84
+ priority: "high" as const,
85
+ },
86
+ webpush: {
87
+ headers: {
88
+ Urgency: "high",
104
89
  },
105
90
  },
106
91
  };
92
+ if (opts.notification) {
93
+ msg.notification = opts.notification;
94
+ }
95
+ const message = { message: msg };
107
96
 
108
97
  const res = await fetch(url, {
109
98
  method: "POST",
@@ -114,13 +103,15 @@ export async function sendPushNotification(opts: PushPayload): Promise<boolean>
114
103
  body: JSON.stringify(message),
115
104
  });
116
105
 
117
- if (!res.ok) {
118
- const err = await res.text();
119
- console.error(`[FCM] Send failed for token ...${opts.fcmToken.slice(-8)}: ${res.status} ${err}`);
120
- // Token invalid/expired — caller should remove it
121
- if (res.status === 404 || res.status === 410) return false;
106
+ if (res.ok) {
107
+ console.log(`[FCM] Sent to ...${opts.fcmToken.slice(-8)} (notification: ${!!opts.notification})`);
108
+ return true;
122
109
  }
123
- return res.ok;
110
+
111
+ const err = await res.text();
112
+ console.error(`[FCM] Send failed for token ...${opts.fcmToken.slice(-8)}: ${res.status} ${err}`);
113
+ if (res.status === 404 || res.status === 410) return false;
114
+ return false;
124
115
  }
125
116
 
126
117
  // ---- Crypto helpers for RS256 JWT signing ----
@@ -1 +1 @@
1
- {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../src/channel.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../src/channel.ts"],"names":[],"mappings":"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"}
@@ -124,6 +124,9 @@ export const botschatPlugin = {
124
124
  return { ok: false, error: new Error(`Encryption failed: ${err}`) };
125
125
  }
126
126
  }
127
+ const notifyPreview = (encrypted && client.notifyPreview && ctx.text)
128
+ ? (ctx.text.length > 100 ? ctx.text.slice(0, 100) + "…" : ctx.text)
129
+ : undefined;
127
130
  client.send({
128
131
  type: "agent.text",
129
132
  sessionKey: ctx.to,
@@ -132,8 +135,9 @@ export const botschatPlugin = {
132
135
  threadId: ctx.threadId?.toString(),
133
136
  messageId,
134
137
  encrypted,
138
+ ...(notifyPreview ? { notifyPreview } : {}),
135
139
  });
136
- console.log(`[botschat][sendText] sent agent.text, encrypted=${encrypted}`);
140
+ console.log(`[botschat][sendText] sent agent.text, encrypted=${encrypted}, notifyPreview=${!!notifyPreview}`);
137
141
  return { ok: true };
138
142
  },
139
143
  sendMedia: async (ctx) => {
@@ -155,6 +159,9 @@ export const botschatPlugin = {
155
159
  return { ok: false, error: new Error(`Encryption failed: ${err}`) };
156
160
  }
157
161
  }
162
+ const notifyPreview = (encrypted && client.notifyPreview && ctx.text)
163
+ ? (ctx.text.length > 100 ? ctx.text.slice(0, 100) + "…" : ctx.text)
164
+ : undefined;
158
165
  if (ctx.mediaUrl) {
159
166
  client.send({
160
167
  type: "agent.media",
@@ -163,6 +170,7 @@ export const botschatPlugin = {
163
170
  caption: text || undefined,
164
171
  messageId,
165
172
  encrypted,
173
+ ...(notifyPreview ? { notifyPreview } : {}),
166
174
  });
167
175
  }
168
176
  else {
@@ -172,6 +180,7 @@ export const botschatPlugin = {
172
180
  text: text,
173
181
  messageId,
174
182
  encrypted,
183
+ ...(notifyPreview ? { notifyPreview } : {}),
175
184
  });
176
185
  }
177
186
  return { ok: true };
@@ -495,7 +504,10 @@ async function handleCloudMessage(msg, ctx) {
495
504
  else {
496
505
  console.log(`[botschat][deliver] no encryption: hasKey=${!!client.e2eKey}, textLen=${text.length}`);
497
506
  }
498
- console.log(`[botschat][deliver] sending: type=${payload.mediaUrl ? "agent.media" : "agent.text"}, encrypted=${encrypted}, messageId=${messageId}`);
507
+ const notifyPreviewText = (encrypted && client.notifyPreview && payload.text)
508
+ ? (payload.text.length > 100 ? payload.text.slice(0, 100) + "…" : payload.text)
509
+ : undefined;
510
+ console.log(`[botschat][deliver] sending: type=${payload.mediaUrl ? "agent.media" : "agent.text"}, encrypted=${encrypted}, messageId=${messageId}, notifyPreview=${!!notifyPreviewText}`);
499
511
  if (payload.mediaUrl) {
500
512
  client.send({
501
513
  type: "agent.media",
@@ -505,6 +517,7 @@ async function handleCloudMessage(msg, ctx) {
505
517
  threadId,
506
518
  messageId,
507
519
  encrypted,
520
+ ...(notifyPreviewText ? { notifyPreview: notifyPreviewText } : {}),
508
521
  });
509
522
  }
510
523
  else if (payload.text) {
@@ -515,6 +528,7 @@ async function handleCloudMessage(msg, ctx) {
515
528
  threadId,
516
529
  messageId,
517
530
  encrypted,
531
+ ...(notifyPreviewText ? { notifyPreview: notifyPreviewText } : {}),
518
532
  });
519
533
  // Detect model-change confirmations and emit model.changed
520
534
  // Handles both formats:
@@ -670,6 +684,15 @@ async function handleCloudMessage(msg, ctx) {
670
684
  }
671
685
  break;
672
686
  }
687
+ case "settings.notifyPreview": {
688
+ const enabled = msg.enabled === true;
689
+ const client = getCloudClient(ctx.accountId);
690
+ if (client) {
691
+ client.notifyPreview = enabled;
692
+ ctx.log?.info(`[${ctx.accountId}] Notification preview ${enabled ? "enabled" : "disabled"}`);
693
+ }
694
+ break;
695
+ }
673
696
  default:
674
697
  break;
675
698
  }